Skip to content
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

Merged
merged 17 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/common/activitycard/ActivityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function ActivityCard({
const DataList: DataListType = {
rate: {
label: ["남긴", "별점 평균"],
img: "/icons/star.svg",
img: "/icons/star_on.svg",
data: myRateAvg?.toFixed(1),
},
review: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/productcard/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function ProductCard({
likeCount,
rate,
}: Props) {
const starIconSrc = "/icons/star.svg";
const starIconSrc = "/icons/star_on.svg";
return (
<div className="flex max-h-[18.3rem] max-w-[16rem] grow flex-col rounded-[1.2rem] border border-black-border bg-black-bg md:max-h-[25.6rem] md:max-w-[24.7rem] lg:max-h-[30.8rem] lg:max-w-[30rem]">
<div className="relative h-[14rem] max-w-[14rem] md:h-[22.7rem] md:max-w-[22.7rem] lg:h-[18.4rem] lg:max-w-[28.4rem]">
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/statisticscard/StatisticsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function StatisticsCard({
label: "별점 평균",
productData: rateData,
avgData: rateAvg,
icon: "/icons/star.svg",
icon: "/icons/star_on.svg",
unit: "점",
bottomDecription: {
higher: "더 높아요!",
Expand Down Expand Up @@ -65,7 +65,7 @@ export default function StatisticsCard({

const result = calculateDifference(productData, avgData, bottomDecription);
return (
<div className="flex h-[8.2rem] min-w-[33.5rem] flex-col justify-center rounded-[1.2rem] border border-[#353542] bg-[#252530] px-[2rem] md:min-h-[16.9rem] md:min-w-[21.8rem] md:items-center md:gap-[2rem] lg:h-[19rem] lg:w-[30rem] lg:items-center lg:gap-[2rem]">
<div className="flex flex-col gap-[0.5rem] rounded-[1.2rem] border border-[#353542] bg-[#252530] p-[2rem] max-[767px]:min-w-[33.5rem] md:w-full md:items-center md:justify-center md:gap-[1.5rem] md:py-[3rem] lg:gap-[2rem]">
<div className="flex flex-row gap-[1rem]">
<span className="text-[1.4rem] text-white md:text-[1.6rem] lg:text-[1.8rem]">
{label}
Expand Down
25 changes: 15 additions & 10 deletions src/components/productdetail/DetailCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ type FavoriteProps = {
className: string;
};

const imageCn = "object-contain";
const imageCn = "object-cover";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스네임들은 왜 따로 변수화 하셨나요? 👀
분기에 의해 달라지는것 같지는 않아 여쭤봅니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇네요..! 여러 곳에서 사용되니깐 변수로 해볼까 하고 작성해봤는데, 말씀하신대로
분기점도 없는데 굳이 변수화 시킬 이유는없는거같아요 ㅋㅋㅋ;
className에 직접 넣는걸로 수정했습니다! 👍


export default function DetailCard({ productData, isMyProduct }: Props) {
const { name, description, image, isFavorite, category } = productData;
const mobileHiddenCn = "hidden md:flex";
const onlyMobileCn = "flex md:hidden";

return (
<div className="flex flex-col items-center gap-[4rem] md:flex-row md:gap-[2rem]">
<div className="relative h-[23.6rem] w-[33.5rem] md:h-[19.7rem] md:w-[28rem] lg:h-[25rem] lg:w-[35.5rem] ">
<div className="flex min-w-[33.5rem] flex-col items-center md:flex-row lg:justify-between">
<div className="relative min-h-[19.7rem] min-w-[28rem] lg:ml-[3rem]">
<Image src={image} fill alt={name} className={imageCn} />
</div>
<div className="flex w-[33.5rem] flex-col md:w-[38.4rem] lg:w-[54.5rem]">
<div className="flex flex-col">
<div className="flex justify-between">
<CategoryBadge size="small" category={category.name} />
<Share className={onlyMobileCn} />
Expand All @@ -48,32 +48,35 @@ export default function DetailCard({ productData, isMyProduct }: Props) {
<Share className={mobileHiddenCn} />
<Favorite isFavorite={isFavorite} className={onlyMobileCn} />
</div>
<div className="text-[1.4rem] text-white lg:text-[1.6rem]">
<div className="text-[1.4rem] text-white lg:max-w-[54.5rem] lg:text-[1.6rem]">
{description}
</div>
<div className="flex flex-col gap-[1.5rem] pt-[2rem] md:flex-row md:gap-[2rem] md:pt-[6rem]">
<BasicButton
label="리뷰 작성하기"
variant="primary"
className={clsx("md:w-[24.6rem] lg:w-[34.5rem]", {
"md:w-[14rem] lg:w-[18.5rem]": isMyProduct,
className={clsx("md:lg:max-w-[34.5rem]", {
"lg:max-w-[18.5rem]": isMyProduct,
})}
/>
{/**TODO: 리뷰 작성 모달, 비로그인 시 로그인 요청 모달*/}
<BasicButton
label="비교하기"
variant="secondary"
className={clsx("md:w-[12.3rem] lg:w-[18rem]", {
"md:w-[10.7rem] lg:w-[16rem]": isMyProduct,
className={clsx("md:max-w-[12.3rem] lg:max-w-[18rem]", {
"md:max-w-[10.7rem] lg:max-w-[16rem]": isMyProduct,
})}
/>
{/**TODO: 비교상품 없을 경우 alert표시, 하나 있을 경우 확인할지 안할지 모달 표시 확인하면 /compare 이동, 두개 있을 경우 비교 상품 교체 모달 비로그인시 로그인 요청 모달*/}
{isMyProduct && (
<BasicButton
label="편집하기"
variant="tertiary"
className="md:w-[10.7rem] lg:w-[16rem]"
className="md:max-w-[10.7rem] lg:max-w-[16rem]"
/>
)}
</div>
{/**TODO: 상품 편집 모달 추가*/}
</div>
</div>
);
Expand All @@ -97,6 +100,7 @@ export function Share({ className }: ShareProps) {
/>
</div>
</button>
{/**TODO: 카카오공유는 배포이후 추가 가능*/}
<button className={buttonCn}>
<div className={imageDivCn}>
<Image
Expand All @@ -107,6 +111,7 @@ export function Share({ className }: ShareProps) {
/>
</div>
</button>
{/**TODO: 클립보드 복사 기능 추가*/}
</div>
);
}
Expand Down
107 changes: 107 additions & 0 deletions src/components/productdetail/MockData.ts
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,
},
],
};
25 changes: 25 additions & 0 deletions src/components/productdetail/ProductDetail.tsx
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 저도 구현에 들어가지 않아서 잘 모르겠지만, cookie에 담기는건 accessToken 뿐이고 유저의 정보(users/me)는 리액트쿼리를 사용해서 캐싱된 데이터를 불러올 것 같아요.
결국 isMyProduct 는 users/me를 불러온 useQuery의 결과의 id 와 productDetailData.writerId 를 비교해야 할 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
);
}
15 changes: 15 additions & 0 deletions src/components/productdetail/ProductDetailPageLayout.tsx
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>
);
}
48 changes: 48 additions & 0 deletions src/components/productdetail/ProductReview.tsx
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
}
29 changes: 29 additions & 0 deletions src/components/productdetail/ProductStatistics.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
}
14 changes: 9 additions & 5 deletions src/components/productdetail/ReviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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]">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

justify-between을 사용하니 content의 길이가 짧을 때 UI가 어색해지네요
content 길이와 상관 없이 좌측 정렬되면 좋을 것 같습니다!
스크린샷 2024-03-19 오후 11 52 47

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 PR에서 수정된건 아닌것 같지만... 별점 ui의 좌측 정렬 시작 지점이 디자인과 약간 다른 것 같습니다!

피그마 시안:
스크린샷 2024-03-19 오후 11 58 37

{rateArray.map((index) => (
<div
Expand All @@ -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>
Expand All @@ -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>
Expand Down
Loading