-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 상품상세화면 페이지UI 구현 #110
The head ref may contain hidden characters: "feat/\uC0C1\uD488\uC0C1\uC138\uD654\uBA74UI"
Changes from 12 commits
0ed4c9b
53559bb
ad84e18
1c2be7d
bcadeb9
e85a770
42a3231
65c6749
f528796
90ce0b0
911579a
83a1298
c883639
92f48b3
3ca86f4
da0723e
f28b15b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { ProductDetail, ReviewResponse } from "@/types/product"; | ||
|
||
export const productDetailData: ProductDetail = { | ||
id: 1, | ||
name: "Sony WH-1000XM3", | ||
description: | ||
"한층 업그레이된 고급 노이즈 캔슬과 상황에 맞게 조정되는 스마트 청취 기능을 갖춘 WH-1000XM3 헤드폰으로 더욱 깊은 고요 속에서 청취할 수 있습니다.", | ||
image: "/images/headset.svg", | ||
rating: 4.9, | ||
reviewCount: 4123, | ||
favoriteCount: 566, | ||
categoryId: 1, | ||
createdAt: "2024-03-18", | ||
updatedAt: "2024-03-18", | ||
writerId: 1, | ||
isFavorite: true, | ||
category: { | ||
id: 1, | ||
name: "전자기기", | ||
}, | ||
categoryMetric: { | ||
rating: 4.1, | ||
favoriteCount: 543, | ||
reviewCount: 3937, | ||
}, | ||
}; | ||
|
||
export const reviewData: ReviewResponse = { | ||
nextCursor: 0, | ||
list: [ | ||
{ | ||
user: { | ||
image: "/images/profile1.svg", | ||
nickname: "테스트1", | ||
id: 1, | ||
}, | ||
reviewImages: [ | ||
{ | ||
source: "/images/test1.jpg", | ||
id: 1, | ||
}, | ||
{ | ||
source: "/images/test2.png", | ||
id: 2, | ||
}, | ||
], | ||
productId: 1, | ||
userId: 1, | ||
updatedAt: "2024-01-29", | ||
createdAt: "2024-01-27", | ||
isLiked: true, | ||
likeCount: 14, | ||
content: | ||
"음질 미칩니다ㅎㅎ 최고예용~ 어플 연동으로 음향 설정 및 설정모드 되고, 설정별로 사운드감이 틀려요 서라운드 느낌까지 들고, 따로는 베이스깐 우퍼 느낌도 있어요", | ||
rating: 5, | ||
id: 1, | ||
}, | ||
{ | ||
user: { | ||
image: "/images/profile1.svg", | ||
nickname: "테스트2", | ||
id: 1, | ||
}, | ||
reviewImages: [], | ||
productId: 1, | ||
userId: 2, | ||
updatedAt: "2024-01-23", | ||
createdAt: "2024-01-24", | ||
isLiked: false, | ||
likeCount: 11, | ||
content: | ||
"전작과 동일하게, 소니 헤드폰 커넥트 애플리케이션을 통한 노이즈 캔슬링 컨트롤이 가능하다. 1000XM2에 있었던 대기압 센서도 그대로 탑재!", | ||
rating: 3, | ||
id: 2, | ||
}, | ||
{ | ||
user: { | ||
image: "/images/profile1.svg", | ||
nickname: "테스트3", | ||
id: 1, | ||
}, | ||
reviewImages: [ | ||
{ | ||
source: "/images/test1.jpg", | ||
id: 1, | ||
}, | ||
{ | ||
source: "/images/test2.png", | ||
id: 2, | ||
}, | ||
{ | ||
source: "/images/testImage.png", | ||
id: 3, | ||
}, | ||
], | ||
productId: 1, | ||
userId: 3, | ||
updatedAt: "2024-01-25", | ||
createdAt: "2024-01-26", | ||
isLiked: true, | ||
likeCount: 42, | ||
content: "구려요", | ||
rating: 1, | ||
id: 3, | ||
}, | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { useEffect, useState } from "react"; | ||
|
||
import DetailCard from "./DetailCard"; | ||
import { productDetailData } from "./MockData"; | ||
|
||
export default function ProductDetail() { | ||
const [cookieid, setCookieId] = useState<number>(0); | ||
|
||
useEffect(() => { | ||
const cookies = Object.fromEntries( | ||
document.cookie.split(";").map((cookie) => cookie.trim().split("=")), | ||
); | ||
setCookieId(Number(cookies["id"])); | ||
}, []); | ||
//TODO: 쿠키는 아마도 기능구현때 store에서 관리 | ||
Comment on lines
+7
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아직 저도 구현에 들어가지 않아서 잘 모르겠지만, cookie에 담기는건 accessToken 뿐이고 유저의 정보(users/me)는 리액트쿼리를 사용해서 캐싱된 데이터를 불러올 것 같아요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아! 저는 로그인 api요청 때 쿠키를 담고, 각 컴포넌트에서 불러오는 방식만 생각했는데, 말씀하신대로 캐시에 담긴 id를 사용하면되니깐 쿠키를 불러올 필요는 없네요. 그러면 쿠키 애드온설치는 일단 pr을 닫아놓고, 우선 기능 구현부터 끝내고나서 스토리북을 만들어봐야겠네요 ㅎㅎ storybookjs/storybook#12489 (comment) << 확실하진 않지만 스토리북에서 리액트쿼리 캐시 데이터 불러오는 방법도 있는거 같네요! |
||
|
||
return ( | ||
<div className="w-full lg:w-[94rem]"> | ||
<DetailCard | ||
productData={productDetailData} | ||
isMyProduct={productDetailData.writerId === cookieid} | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import AddProductButton from "../common/button/AddProductButton"; | ||
import ProductDetail from "./ProductDetail"; | ||
import ProductReview from "./ProductReview"; | ||
import ProductStatistics from "./ProductStatistics"; | ||
|
||
export default function ProductDetailPageLayout() { | ||
return ( | ||
<main className="_flex-col-center gap-[6rem] bg-[#1C1C22] px-[2rem] py-[3rem] md:px-[3rem] md:py-[4rem] lg:py-[6rem]"> | ||
<ProductDetail /> | ||
<ProductStatistics /> | ||
<ProductReview /> | ||
<AddProductButton /> | ||
</main> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useEffect, useState } from "react"; | ||
|
||
import { filterBy } from "@/constants/filterBy"; | ||
|
||
import Dropdown from "../common/dropdown/Dropdown"; | ||
import { reviewData } from "./MockData"; | ||
import ReviewCard from "./ReviewCard"; | ||
|
||
export default function ProductReview() { | ||
const [cookieid, setCookieId] = useState<number>(0); | ||
|
||
useEffect(() => { | ||
const cookies = Object.fromEntries( | ||
document.cookie.split(";").map((cookie) => cookie.trim().split("=")), | ||
); | ||
setCookieId(Number(cookies["id"])); | ||
}, []); | ||
//TODO: 쿠키는 아마도 기능구현때 store에서 관리 | ||
Comment on lines
+11
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기도 리액트 쿼리로 캐싱된 유저 정보를 불러와서 비교할 것 같습니다! |
||
|
||
return ( | ||
<div className="w-full lg:w-[94rem]"> | ||
{/**TODO: 리뷰 목록 무한 스크롤 구현 */} | ||
<div className="flex min-w-[33.5rem] items-center justify-between pb-[3rem] "> | ||
<span className="text-[1.8rem] text-white md:text-[1.6rem] lg:text-[2rem]"> | ||
상품 리뷰 | ||
</span> | ||
<Dropdown | ||
items={filterBy} | ||
defaultItem={filterBy[0]} | ||
onSelect={(item) => console.log(`선택된 항목: ${item.name}`)} | ||
> | ||
{/**TODO: 선택된 항목 별 정렬 */} | ||
<Dropdown.Button variant={"small"} /> | ||
<Dropdown.List /> | ||
</Dropdown> | ||
</div> | ||
<div className="flex flex-col gap-[1.5rem] lg:gap-[2rem]"> | ||
{reviewData.list.map((data) => ( | ||
<ReviewCard | ||
reviewData={data} | ||
isMyReview={data.userId === cookieid} | ||
key={data.id} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금은 mock 데이터를 쓰고 있지만 추후엔 상위에서 내려주는 productDetail 정도의 객체를 받을 것 같네요! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import StatisticsCard from "../common/statisticscard/StatisticsCard"; | ||
import { productDetailData } from "./MockData"; | ||
|
||
export default function ProductStatistics() { | ||
return ( | ||
<div className="flex w-full flex-col lg:w-[94rem] lg:pt-[2rem]"> | ||
<span className="pb-[3rem] text-[1.8rem] text-white md:text-[1.6rem] lg:text-[2rem]"> | ||
상품 통계 | ||
</span> | ||
<div className="flex flex-col gap-[1.5rem] md:flex-row lg:gap-[2rem] "> | ||
<StatisticsCard | ||
type="rate" | ||
rateData={productDetailData.rating} | ||
rateAvg={productDetailData.categoryMetric.rating} | ||
/> | ||
<StatisticsCard | ||
type="like" | ||
likeData={productDetailData.favoriteCount} | ||
likeAvg={productDetailData.categoryMetric.favoriteCount} | ||
/> | ||
<StatisticsCard | ||
type="review" | ||
reviewData={productDetailData.reviewCount} | ||
reviewAvg={productDetailData.categoryMetric.reviewCount} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ export default function ReviewCard({ reviewData, isMyReview }: Props) { | |
const starOffIconSrc = "/icons/star_off.svg"; | ||
|
||
const handleButtonClick = () => { | ||
console.log("TODO:좋아요 개수 증가"); | ||
console.log("TODO:좋아요 개수 증가 비로그인 시 로그인요청 모달"); | ||
}; | ||
|
||
const reviewerData = { | ||
|
@@ -29,12 +29,15 @@ export default function ReviewCard({ reviewData, isMyReview }: Props) { | |
followersCount: 162, | ||
reviewCount: 37, | ||
}; | ||
//TODO: 유저랭크, 팔로워 카운터, 리뷰카운터를 백엔드에서 제공안해서, api 수정 요청하거나 userId로 유저 정보/랭킹 조회 데이터 가져와야함 | ||
//TODO: 유저랭크, 팔로워 카운터, 리뷰카운터를 백엔드에서 제공X, qna 확인요청한 상태 | ||
|
||
return ( | ||
<div className="flex flex-col gap-[3rem] rounded-[1.2rem] border border-black-border bg-black-bg p-[2rem] md:flex-row lg:p-[3rem]"> | ||
<div className="flex min-w-[33.5rem] flex-col justify-between gap-[3rem] rounded-[1.2rem] border border-black-border bg-black-bg p-[2rem] md:flex-row lg:p-[3rem]"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵! 수정했습니다. 👍 |
||
<div className="flex min-w-[17rem] flex-row justify-between md:flex-col md:justify-normal md:gap-[1rem] lg:gap-[1.5rem]"> | ||
<ReviewerProfile reviewerData={reviewerData} /> | ||
<button> | ||
<ReviewerProfile reviewerData={reviewerData} /> | ||
</button> | ||
{/**TODO: 버튼 클릭 시 유저 프로필화면 이동 /user/{userId} */} | ||
<div className="flex items-end md:justify-center"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{rateArray.map((index) => ( | ||
<div | ||
|
@@ -51,7 +54,7 @@ export default function ReviewCard({ reviewData, isMyReview }: Props) { | |
))} | ||
</div> | ||
</div> | ||
<div className="flex min-w-[29.5rem] flex-col gap-[2rem] lg:min-w-[65rem]"> | ||
<div className="flex flex-col justify-end gap-[2rem] lg:w-[65rem]"> | ||
<span className="text-[1.2rem] text-[white] lg:text-[1.6rem]"> | ||
{content} | ||
</span> | ||
|
@@ -77,6 +80,7 @@ export default function ReviewCard({ reviewData, isMyReview }: Props) { | |
<div className="flex gap-[1rem] font-light text-gray-100 underline"> | ||
<button>수정</button> | ||
<button>삭제</button> | ||
{/**TODO: 수정모달 추가, 삭제 alert추가 */} | ||
</div> | ||
)} | ||
</div> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클래스네임들은 왜 따로 변수화 하셨나요? 👀
분기에 의해 달라지는것 같지는 않아 여쭤봅니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그렇네요..! 여러 곳에서 사용되니깐 변수로 해볼까 하고 작성해봤는데, 말씀하신대로
분기점도 없는데 굳이 변수화 시킬 이유는없는거같아요 ㅋㅋㅋ;
className에 직접 넣는걸로 수정했습니다! 👍