Skip to content

Commit 5d1c82f

Browse files
authored
Merge pull request #156 from YAPP-Github/feature/PRODUCT-274
2 parents bb06fb3 + d7a1479 commit 5d1c82f

File tree

8 files changed

+304
-106
lines changed

8 files changed

+304
-106
lines changed

src/app/(store)/_api/shop/shop.api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type StoreCheersResponse,
66
type StoreDetailResponse,
77
type StoreImagesResponse,
8+
type StoreTagsResponse,
89
} from "./shop.types";
910

1011
export const STORE_ERROR_CODES = {
@@ -69,3 +70,14 @@ export const getCheeredMember = async (): Promise<CheeredMemberResponse> => {
6970
.get("api/shops/cheered-member")
7071
.json<CheeredMemberResponse>();
7172
};
73+
74+
/**
75+
* 가게별 태그 조회 API
76+
* @params storeId 조회할 가게 ID
77+
* @returns 가게별 태그 정보
78+
*/
79+
export const getStoreTags = async (
80+
storeId: number
81+
): Promise<StoreTagsResponse> => {
82+
return await http.get(`api/shops/${storeId}/tags`).json<StoreTagsResponse>();
83+
};

src/app/(store)/_api/shop/shop.queries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getStoreCheers,
66
getStoreDetail,
77
getStoreImages,
8+
getStoreTags,
89
} from "./shop.api";
910

1011
export const storeQueryKeys = {
@@ -14,6 +15,7 @@ export const storeQueryKeys = {
1415
[...storeQueryKeys.all, storeId, "cheers", size] as const,
1516
images: (storeId: number) =>
1617
[...storeQueryKeys.all, storeId, "images"] as const,
18+
tags: (storeId: number) => [...storeQueryKeys.all, storeId, "tags"] as const,
1719
};
1820

1921
export const storeDetailQueryOptions = (storeId: number) =>
@@ -43,3 +45,9 @@ export const cheeredMemberQueryOptions = () =>
4345
queryKey: cheeredMemberQueryKeys.all,
4446
queryFn: () => getCheeredMember(),
4547
});
48+
49+
export const storeTagsQueryOptions = (storeId: number) =>
50+
queryOptions({
51+
queryKey: storeQueryKeys.tags(storeId),
52+
queryFn: () => getStoreTags(storeId),
53+
});

src/app/(store)/_api/shop/shop.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type StoreCheers = {
1515
memberId: number;
1616
memberNickname: string;
1717
description: string;
18+
tags: string[];
1819
};
1920

2021
export type StoreCheersResponse = { cheers: StoreCheers[] };
@@ -34,3 +35,7 @@ export type CheeredStore = {
3435
export type CheeredMemberResponse = {
3536
stores: CheeredStore[];
3637
};
38+
39+
export type StoreTagsResponse = {
40+
tags: string[];
41+
};
Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
11
import { style } from "@vanilla-extract/css";
22

3-
import { colors, radius, semantic } from "@/styles";
3+
import { radius } from "@/styles";
44

55
export const storeCheersContainer = style({
66
paddingTop: "2.4rem",
77
paddingBottom: "4rem",
88
});
99

10-
export const cheerCardProfileImage = style({
11-
width: "2.8rem",
12-
height: "2.8rem",
10+
export const cheerCard = style({
11+
width: "100%",
12+
borderRadius: radius[160],
13+
overflow: "hidden",
14+
});
15+
16+
export const cheerCardHeader = style({
17+
display: "flex",
18+
alignItems: "center",
19+
gap: "0.8rem",
20+
padding: "1.2rem 2rem",
1321
});
1422

15-
export const cheerCardDivider = style({
16-
width: "0.2rem",
17-
height: "auto",
18-
alignSelf: "stretch",
19-
flexShrink: 0,
20-
backgroundColor: colors.coolNeutral[97],
21-
marginLeft: "1.4rem",
22-
marginRight: "2.1rem",
23+
export const cheerCardAvatar = style({
24+
width: "2.4rem",
25+
height: "2.4rem",
2326
});
2427

2528
export const cheerCardContent = style({
26-
width: "100%",
27-
height: "100%",
28-
padding: "1.6rem",
29-
backgroundColor: semantic.background.grayLight,
30-
borderRadius: radius[160],
29+
padding: "2rem",
30+
paddingBottom: "1.6rem",
3131
});
3232

3333
export const cheerCardContentText = style({
34+
width: "100%",
3435
selectors: {
3536
"&[data-long-text='true'][data-expanded='false']": {
3637
display: "-webkit-box",
@@ -41,3 +42,17 @@ export const cheerCardContentText = style({
4142
},
4243
},
4344
});
45+
46+
export const tag = style({
47+
display: "flex",
48+
alignItems: "center",
49+
gap: "0.4rem",
50+
padding: "0.6rem 0.8rem",
51+
backgroundColor: "#ffffff",
52+
borderRadius: "999px",
53+
border: "1px solid #f0f0f0",
54+
});
55+
56+
export const tagIcon = style({
57+
fontSize: "1.2rem",
58+
});

src/app/(store)/stores/[storeId]/_components/StoreCheers/StoreCheers.tsx

Lines changed: 104 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import { Suspense } from "@suspensive/react";
44
import { useSuspenseQuery } from "@tanstack/react-query";
5-
import Link from "next/link";
5+
import { slice } from "es-toolkit/compat";
6+
import { motion } from "motion/react";
7+
import Image from "next/image";
68
import { useState } from "react";
79

810
import { storeCheersQueryOptions } from "@/app/(store)/_api/shop";
@@ -11,10 +13,13 @@ import { Button } from "@/components/ui/Button";
1113
import { Skeleton } from "@/components/ui/Skeleton";
1214
import { Spacer } from "@/components/ui/Spacer";
1315
import { HStack, VStack } from "@/components/ui/Stack";
16+
import { Tag } from "@/components/ui/Tag";
1417
import { Text } from "@/components/ui/Text";
1518
import { TextButton } from "@/components/ui/TextButton";
19+
import { ALL_TAGS } from "@/constants/tag.constants";
1620

1721
import * as styles from "./StoreCheers.css";
22+
import { getContentBackgroundColor, getHeaderBackgroundColor } from "./utils";
1823

1924
export const StoreCheers = ({ storeId }: { storeId: number }) => {
2025
return (
@@ -32,17 +37,21 @@ export const StoreCheers = ({ storeId }: { storeId: number }) => {
3237

3338
const CHEERS_SIZE = 50;
3439

40+
const ITEMS_PER_PAGE = 3;
41+
42+
const THEMES = ["yellow", "pink", "blue"] as const;
43+
type Theme = (typeof THEMES)[number];
44+
3545
const CheerContent = ({ storeId }: { storeId: number }) => {
3646
const {
3747
data: { cheers },
3848
} = useSuspenseQuery(storeCheersQueryOptions(storeId, CHEERS_SIZE));
3949

4050
const [visibleCount, setVisibleCount] = useState(3);
4151

42-
const ITEMS_PER_PAGE = 3;
4352
const totalItems = cheers.length;
4453
const isAllVisible = visibleCount >= totalItems;
45-
const shouldShowToggleButton = totalItems > ITEMS_PER_PAGE;
54+
const showToggleButton = totalItems > ITEMS_PER_PAGE;
4655

4756
const handleToggle = () => {
4857
if (isAllVisible) {
@@ -59,16 +68,17 @@ const CheerContent = ({ storeId }: { storeId: number }) => {
5968
return (
6069
<VStack>
6170
<VStack gap={24}>
62-
{visibleCards.map(card => (
71+
{visibleCards.map((card, index) => (
6372
<CheerCard
6473
key={card.id}
6574
author={card.memberNickname}
6675
content={card.description}
67-
memberId={card.memberId}
76+
tags={card.tags}
77+
theme={THEMES[index % THEMES.length] as Theme}
6878
/>
6979
))}
7080
</VStack>
71-
{shouldShowToggleButton && (
81+
{showToggleButton && (
7282
<>
7383
<Spacer size={20} />
7484
<Button
@@ -82,64 +92,114 @@ const CheerContent = ({ storeId }: { storeId: number }) => {
8292
</>
8393
)}
8494

85-
<Spacer size={shouldShowToggleButton ? 12 : 20} />
86-
87-
<Link href={`/stores/register?storeId=${storeId}`}>
88-
<Button variant='primary' size='large' fullWidth>
89-
가게 응원하기
90-
</Button>
91-
</Link>
95+
<Spacer size={showToggleButton ? 12 : 20} />
9296
</VStack>
9397
);
9498
};
9599

96100
const CheerCard = ({
97101
author,
98102
content,
99-
memberId,
103+
tags,
104+
theme,
100105
}: {
101106
author: string;
102107
content: string;
103-
memberId: number;
108+
tags: string[];
109+
theme: Theme;
104110
}) => {
105111
const [isExpanded, setIsExpanded] = useState(false);
106-
107112
const isLongText = content.length > 50;
108113

114+
const selectedTags = ALL_TAGS.filter(tag => tags.includes(tag.name));
115+
const visibleTags = isExpanded ? selectedTags : slice(selectedTags, 0, 2);
116+
const additionalTagsCount = Math.max(
117+
0,
118+
selectedTags.length - visibleTags.length
119+
);
120+
const showAdditionalTags = additionalTagsCount > 0 && !isExpanded;
121+
109122
return (
110-
<VStack gap={12}>
111-
<HStack align='center' gap={8}>
112-
<Avatar memberId={memberId} className={styles.cheerCardProfileImage} />
123+
<motion.div
124+
className={styles.cheerCard}
125+
onClick={() => setIsExpanded(!isExpanded)}
126+
role='button'
127+
tabIndex={0}
128+
transition={{ duration: 0.3 }}
129+
whileTap={{ scale: 0.99 }}
130+
>
131+
<div
132+
className={styles.cheerCardHeader}
133+
style={{
134+
backgroundColor: getHeaderBackgroundColor(theme),
135+
}}
136+
>
137+
<Avatar
138+
// TODO: 추후 theme 지정 가능하게끔 수정
139+
memberId={THEMES.findIndex(t => t === theme)}
140+
className={styles.cheerCardAvatar}
141+
/>
113142
<Text as='span' typo='body1Sb' color='text.normal'>
114143
{author}
115144
</Text>
116-
</HStack>
117-
118-
<HStack align='stretch'>
119-
<hr className={styles.cheerCardDivider} />
120-
<VStack className={styles.cheerCardContent} gap={4} align='start'>
121-
<Text
122-
as='p'
123-
typo='body2Rg'
124-
color='text.normal'
125-
className={styles.cheerCardContentText}
126-
data-expanded={isExpanded}
127-
data-long-text={isLongText}
128-
>
129-
{content}
130-
</Text>
131-
{isLongText && (
132-
<TextButton
133-
size='small'
134-
variant='assistive'
135-
onClick={() => setIsExpanded(!isExpanded)}
145+
</div>
146+
147+
<div
148+
className={styles.cheerCardContent}
149+
style={{
150+
backgroundColor: getContentBackgroundColor(theme),
151+
}}
152+
>
153+
<VStack gap={8} align='start'>
154+
<VStack gap={4} align='start'>
155+
<Text
156+
as='p'
157+
typo='body2Rg'
158+
color='text.normal'
159+
className={styles.cheerCardContentText}
160+
data-expanded={isExpanded}
161+
data-long-text={isLongText}
136162
>
137-
{isExpanded ? "접기" : "더보기"}
138-
</TextButton>
139-
)}
163+
{content}
164+
</Text>
165+
{isLongText && (
166+
<TextButton
167+
size='small'
168+
variant='assistive'
169+
onClick={() => setIsExpanded(!isExpanded)}
170+
>
171+
{isExpanded ? "접기" : "더보기"}
172+
</TextButton>
173+
)}
174+
</VStack>
175+
176+
<HStack gap={8} align='start' wrap='wrap'>
177+
{visibleTags.map(tag => (
178+
<Tag key={tag.name}>
179+
<Image
180+
src={tag.iconUrl}
181+
alt={tag.label}
182+
width={16}
183+
height={16}
184+
className={styles.tagIcon}
185+
/>
186+
<Text as='span' typo='caption1Sb' color='text.primary'>
187+
{tag.label}
188+
</Text>
189+
</Tag>
190+
))}
191+
192+
{showAdditionalTags && (
193+
<Tag>
194+
<Text as='span' typo='caption1Sb' color='text.primary'>
195+
+{additionalTagsCount}
196+
</Text>
197+
</Tag>
198+
)}
199+
</HStack>
140200
</VStack>
141-
</HStack>
142-
</VStack>
201+
</div>
202+
</motion.div>
143203
);
144204
};
145205

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Theme = "yellow" | "pink" | "blue";
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type Theme } from "./types";
2+
3+
export const getCheerCardTheme = (memberId: number) => {
4+
return ["yellow", "pink", "blue"][memberId % 3];
5+
};
6+
7+
// 색상 테마별 배경색 헬퍼 함수
8+
export const getHeaderBackgroundColor = (colorName: Theme) => {
9+
switch (colorName) {
10+
case "yellow":
11+
return "#fceb9c";
12+
case "pink":
13+
return "#fabdb8";
14+
case "blue":
15+
return "#b2dfff";
16+
default:
17+
return "#fceb9c";
18+
}
19+
};
20+
21+
export const getContentBackgroundColor = (colorName: Theme) => {
22+
switch (colorName) {
23+
case "yellow":
24+
return "#fef8dd";
25+
case "pink":
26+
return "#fde5e3";
27+
case "blue":
28+
return "#e0f2ff";
29+
default:
30+
return "#fef8dd";
31+
}
32+
};

0 commit comments

Comments
 (0)