Skip to content

Commit a04e9a4

Browse files
committed
refactor(mentor): 리뷰카드 -> 주스탄드기반 합성컴포넌트
1 parent 60ab8e4 commit a04e9a4

File tree

8 files changed

+284
-128
lines changed

8 files changed

+284
-128
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"react-hook-form": "^7.59.0",
3535
"tailwind-merge": "^3.3.1",
3636
"tailwindcss-animate": "^1.0.7",
37+
"use-sync-external-store": "^1.5.0",
3738
"zod": "^4.0.5",
3839
"zustand": "^5.0.5"
3940
},
Lines changed: 95 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,119 @@
1-
import React from 'react';
2-
31
import ShowMoreBtn from '@/assets/icons/showMoreBtn.svg';
2+
import Star from '@/assets/icons/star.svg';
43
import { Badge } from '@/components/ui/badge';
54
import { Button } from '@/components/ui/button';
6-
import useClickToggle from '@/hooks/useClickToggle';
75
import { cn } from '@/lib/utils';
6+
import useReviewCardStore from '@/stores/reviewCardStore';
7+
8+
import {
9+
ReviewCardProps,
10+
UserHeaderProps,
11+
TagAndRatingProps,
12+
ReviewBodyProps,
13+
ToggleButtonProps,
14+
} from './ReviewCardTypes';
815

916
/*
1017
여기 저 혼자만 사용하는 것 같아서
11-
제가 임의로 간격이나 이런 수치들 조금 건드렸습니다.
18+
제가 임의로 건드렸습니다.
1219
혹시 다른 분들도 쓰게 된다면 말씀해주세요
1320
*/
14-
15-
interface ReviewCardProps {
16-
userIcon: React.ReactNode; // 유저 아이콘
17-
username: string; // 유저 이름
18-
timeAgo: string; // 작성 시간
19-
tags: string[]; // 태그 목록
20-
rating: React.ReactNode; // 별점 영역 slot
21-
likeSlot: React.ReactNode; // 좋아요 버튼 slot
22-
menuSlot: React.ReactNode; // 메뉴 버튼 slot
23-
reviewText?: string; // 리뷰 목록
24-
flavorSliderSlot?: React.ReactNode; // 슬라이더
25-
className?: string; // 컨테이너 클래스
21+
export function ReviewCard({ children }: ReviewCardProps) {
22+
return (
23+
<div className='rounded-xl bg-white p-4 md:p-8 xl:p-4 xl:px-6 shadow-sm border border-gray-300 md:pb-6 xl:pb-5 w-full xl:w-[800px]'>
24+
{children}
25+
</div>
26+
);
2627
}
2728

28-
export function ReviewCard({
29-
userIcon,
30-
username,
31-
timeAgo,
32-
tags,
33-
rating,
34-
likeSlot,
35-
menuSlot,
36-
reviewText,
37-
flavorSliderSlot,
38-
className,
39-
}: ReviewCardProps) {
40-
const { isOpen, onToggle } = useClickToggle();
41-
const cardTransition = cn('overflow-hidden transition-all duration-500 ease-in-out', {
42-
'opacity-100 translate-y-0 max-h-[1000px]': isOpen,
43-
'opacity-0 -translate-y-4 max-h-0': !isOpen,
44-
});
29+
ReviewCard.UserHeader = function UserHeader({ userIcon, reviewId, children }: UserHeaderProps) {
30+
const username = useReviewCardStore((state) => state.allReviews[reviewId]?.user.name);
31+
const timeAgo = useReviewCardStore((state) => state.allReviews[reviewId]?.updatedAt);
4532

4633
return (
47-
<div
48-
className={cn(
49-
'w-full rounded-xl bg-white p-4 md:p-8 xl:p-4 xl:px-6 shadow-sm border border-gray-300 md:pb-6 xl:pb-5',
50-
className,
51-
)}
52-
>
53-
{/* 상단: 유저 정보 & 우측 slot */}
54-
<div className='flex justify-between items-start'>
55-
<div className='flex items-center gap-4'>
56-
<div className='w-10 h-10 md:w-16 md:h-16 rounded-full bg-gray-200 overflow-hidden'>
57-
{userIcon}
58-
</div>
59-
<div className='flex flex-col'>
60-
<span className='custom-text-lg-semibold text-gray-900'>{username}</span>
61-
<span className='custom-text-md-regular text-gray-500'>{timeAgo}</span>
62-
</div>
34+
<div className='flex justify-between items-start'>
35+
<div className='flex items-center gap-4'>
36+
<div className='w-10 h-10 md:w-16 md:h-16 rounded-full bg-gray-200 overflow-hidden'>
37+
{userIcon}
6338
</div>
64-
65-
{/* 좋아요 & 메뉴 */}
66-
<div className='flex items-center gap-2'>
67-
{likeSlot}
68-
{menuSlot}
39+
<div className='flex flex-col'>
40+
<span className='custom-text-lg-semibold text-gray-900'>{username}</span>
41+
<span className='custom-text-md-regular text-gray-500'>{timeAgo}</span>
6942
</div>
7043
</div>
7144

72-
{/* 태그 & 별점 */}
73-
<div className='mt-3 flex justify-between items-center'>
74-
<div className='flex flex-wrap gap-2'>
75-
{tags.map((tag) => (
76-
<Badge
77-
key={tag + username}
78-
className='mt-4 rounded-full bg-white border-gray-300 px-[10px] py-[6px] md:px-[15px] md:py-2 custom-text-lg-regular text-gray-700'
79-
variant='flavor'
80-
>
81-
{tag}
82-
</Badge>
83-
))}
84-
</div>
85-
<Badge variant='star'>{rating}</Badge>
45+
{/* 좋아요 & 메뉴 */}
46+
<div className='flex items-center gap-2'>{children}</div>
47+
</div>
48+
);
49+
};
50+
51+
ReviewCard.TagAndRating = function TagAndRating({ reviewId }: TagAndRatingProps) {
52+
const tags = useReviewCardStore((state) => state.allReviews[reviewId]?.aroma ?? []);
53+
const rating = useReviewCardStore((state) => state.allReviews[reviewId]?.rating);
54+
return (
55+
<div className='mt-3 flex justify-between items-center'>
56+
<div className='flex flex-wrap gap-2'>
57+
{tags.map((tag) => (
58+
<Badge
59+
key={tag}
60+
className='mt-4 rounded-full bg-white border-gray-300 px-[10px] py-[6px] md:px-[15px] md:py-2 custom-text-lg-regular text-gray-700'
61+
variant='flavor'
62+
>
63+
{tag}
64+
</Badge>
65+
))}
8666
</div>
67+
<Badge variant='star' className='inline-flex gap-1 items-center'>
68+
<Star className='size-3 md:size-4 md:mt-[-2px]' /> {rating}
69+
</Badge>
70+
</div>
71+
);
72+
};
8773

88-
{/* 리뷰 텍스트 */}
89-
{reviewText && (
90-
<p
91-
className={cn(
92-
'mt-9 text-[14px] md:text-[16px] leading-6 md:leading-[26px] text-gray-800',
93-
cardTransition,
94-
)}
95-
>
96-
{reviewText}
97-
</p>
98-
)}
74+
ReviewCard.ReviewBody = function ReviewBody({ reviewId, flavorSliderSlot }: ReviewBodyProps) {
75+
const reviewText = useReviewCardStore((state) => state.allReviews[reviewId]?.content);
76+
const isOpen = useReviewCardStore((state) => state.allReviews[reviewId]?.isOpen);
9977

100-
{/* 슬라이더 */}
101-
{flavorSliderSlot && (
102-
<div className={cn('mt-4 md:mt-6 xl:mt-5', cardTransition)}>{flavorSliderSlot}</div>
103-
)}
104-
<Button
105-
size={null} //버튼 디폴트 덮어씌우기
106-
width={null}
107-
variant='onlyCancel'
108-
onClick={onToggle}
78+
const cardTransition = cn('overflow-hidden transition-all duration-500 ease-in-out', {
79+
'opacity-100 translate-y-0 max-h-[1000px]': isOpen,
80+
'opacity-0 -translate-y-4 max-h-0': !isOpen,
81+
});
82+
83+
if (!reviewText && !flavorSliderSlot) null;
84+
85+
return (
86+
<>
87+
<p
10988
className={cn(
110-
'border-0 mx-auto [&_svg]:w-[30px] [&_svg]:h-[30px] block transition-all duration-500 ease-in-out mt-[-30px]',
111-
{
112-
'scale-y-[-1] mt-0': isOpen,
113-
},
89+
'mt-9 text-[14px] md:text-[16px] leading-6 md:leading-[26px] text-gray-800',
90+
cardTransition,
11491
)}
11592
>
116-
<ShowMoreBtn />
117-
</Button>
118-
</div>
93+
{reviewText}
94+
</p>
95+
<div className={cn('mt-4 md:mt-6 xl:mt-5', cardTransition)}>{flavorSliderSlot}</div>
96+
</>
11997
);
120-
}
98+
};
99+
100+
ReviewCard.ToggleButton = function TogleButton({ reviewId }: ToggleButtonProps) {
101+
const isOpen = useReviewCardStore((state) => state.allReviews[reviewId]?.isOpen);
102+
const toggleReviewOpen = useReviewCardStore((state) => state.toggleReviewOpen);
103+
return (
104+
<Button
105+
size={null} //버튼 디폴트 덮어씌우기
106+
width={null}
107+
variant='onlyCancel'
108+
onClick={() => toggleReviewOpen(reviewId)}
109+
className={cn(
110+
'border-0 mx-auto [&_svg]:w-[30px] [&_svg]:h-[30px] block transition-all duration-500 ease-in-out mt-[-30px]',
111+
{
112+
'scale-y-[-1] mt-0': isOpen,
113+
},
114+
)}
115+
>
116+
<ShowMoreBtn />
117+
</Button>
118+
);
119+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ReactNode } from 'react';
2+
3+
interface ReviewCardProps {
4+
children: ReactNode;
5+
}
6+
7+
interface UserHeaderProps {
8+
userIcon: ReactNode;
9+
reviewId: string;
10+
children: ReactNode;
11+
}
12+
13+
interface TagAndRatingProps {
14+
reviewId: string;
15+
}
16+
17+
interface ReviewBodyProps {
18+
reviewId: string;
19+
flavorSliderSlot: ReactNode;
20+
}
21+
22+
interface ToggleButtonProps {
23+
reviewId: string;
24+
}
25+
26+
export type {
27+
ReviewCardProps,
28+
UserHeaderProps,
29+
TagAndRatingProps,
30+
ReviewBodyProps,
31+
ToggleButtonProps,
32+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useEffect } from 'react';
2+
3+
import { ReviewCard } from '@/components/common/card/ReviewCard';
4+
import UserDefaultImg from '@/components/common/UserDefaultImg';
5+
import FlavorSliderList from '@/components/wineDetail/FlavorSliderList';
6+
import Kebab from '@/components/wineDetail/Kebab';
7+
import LikeButton from '@/components/wineDetail/LikeButton';
8+
import useReviewCardStore from '@/stores/reviewCardStore';
9+
10+
//컨텍스트말고 주스탄드 기반 컴파운드 패턴
11+
interface Props {
12+
content: string;
13+
user: {
14+
name: string;
15+
};
16+
updatedAt: string;
17+
aroma: string[];
18+
rating: string;
19+
lightBold: number;
20+
smoothTannic: number;
21+
drySweet: number;
22+
softAcidic: number;
23+
id: string;
24+
}
25+
26+
function WineReviewCard({ review }: { review: Props }) {
27+
const setReviews = useReviewCardStore((state) => state.setReviews);
28+
29+
useEffect(() => {
30+
setReviews(review);
31+
}, []);
32+
33+
const { id, lightBold, smoothTannic, drySweet, softAcidic } = review;
34+
35+
return (
36+
<ReviewCard>
37+
<ReviewCard.UserHeader
38+
userIcon={<UserDefaultImg className='size-10 md:size-16' />}
39+
reviewId={id}
40+
>
41+
<LikeButton isLike={false} />
42+
<Kebab />
43+
</ReviewCard.UserHeader>
44+
<ReviewCard.TagAndRating reviewId={id}></ReviewCard.TagAndRating>
45+
<ReviewCard.ReviewBody
46+
reviewId={id}
47+
flavorSliderSlot={
48+
<FlavorSliderList
49+
lightBold={lightBold}
50+
smoothTannic={smoothTannic}
51+
drySweet={drySweet}
52+
softAcidic={softAcidic}
53+
/>
54+
}
55+
></ReviewCard.ReviewBody>
56+
<ReviewCard.ToggleButton reviewId={id} />
57+
</ReviewCard>
58+
);
59+
}
60+
61+
export default WineReviewCard;

src/components/wineDetail/mock.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,22 @@ const review1 = {
1111
smoothTannic: 40,
1212
drySweet: 50,
1313
softAcidic: 90,
14-
id: Math.random() * 100,
14+
id: '1',
15+
};
16+
17+
const review2 = {
18+
...review1,
19+
id: '2',
20+
};
21+
22+
const review3 = {
23+
...review1,
24+
id: '3',
25+
};
26+
27+
const review4 = {
28+
...review1,
29+
id: '4',
1530
};
1631

1732
const wineInfo = {
@@ -20,8 +35,8 @@ const wineInfo = {
2035
region: 'Western Cape, South Africa',
2136
price: 360000,
2237
};
23-
const testReviews = [review1, review1, review1, review1];
24-
export { wineInfo, testReviews };
38+
const testReviews = [review1, review2, review3, review4];
39+
export { wineInfo, testReviews, review1 };
2540

2641
/*--------------응답 예시------------------*/
2742
// const example = {

0 commit comments

Comments
 (0)