Skip to content

Commit e5fa487

Browse files
committed
feat: 가게 상세 페이지 기본 ui 구현
1 parent 2acf41e commit e5fa487

File tree

13 files changed

+492
-0
lines changed

13 files changed

+492
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { style } from "@vanilla-extract/css";
2+
3+
import { colors, radius, semantic } from "@/styles";
4+
5+
export const storeCheersContainer = style({
6+
paddingTop: "2.4rem",
7+
paddingBottom: "4rem",
8+
});
9+
10+
export const cheerCardProfileImage = style({
11+
width: "2.8rem",
12+
height: "2.8rem",
13+
borderRadius: "50%",
14+
});
15+
16+
export const cheerCardDivider = style({
17+
width: "0.2rem",
18+
height: "auto",
19+
alignSelf: "stretch",
20+
flexShrink: 0,
21+
backgroundColor: colors.coolNeutral[97],
22+
marginLeft: "1.4rem",
23+
marginRight: "2.1rem",
24+
});
25+
26+
export const cheerCardContent = style({
27+
width: "100%",
28+
height: "100%",
29+
padding: "1.6rem",
30+
backgroundColor: semantic.background.grayLight,
31+
borderRadius: radius[160],
32+
});
33+
34+
export const cheerCardContentText = style({
35+
selectors: {
36+
"&[data-long-text='true'][data-expanded='false']": {
37+
display: "-webkit-box",
38+
WebkitLineClamp: 4,
39+
WebkitBoxOrient: "vertical",
40+
overflow: "hidden",
41+
textOverflow: "ellipsis",
42+
},
43+
},
44+
});
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useState } from "react";
5+
6+
import { Button } from "@/components/ui/Button";
7+
import { Spacer } from "@/components/ui/Spacer";
8+
import { HStack, VStack } from "@/components/ui/Stack";
9+
import { Text } from "@/components/ui/Text";
10+
import { TextButton } from "@/components/ui/TextButton";
11+
12+
import * as styles from "./StoreCheers.css";
13+
14+
const MOCK_CHEER_CARDS = [
15+
{
16+
author: "구마",
17+
content: "짧은 응원 내용입니다.",
18+
},
19+
{
20+
author: "감자",
21+
content:
22+
"정말 긴 응원 내용입니다. 이 가게는 정말 맛있고 사장님도 친절하시고 음식도 정성스럽게 만들어주시고 가격도 저렴해서 자주 와서 먹고 있습니다. 앞으로도 계속 번창하시길 바라며 많은 사람들이 이 맛집을 알아봤으면 좋겠습니다. 진짜 최고의 가게입니다!",
23+
},
24+
{
25+
author: "호박",
26+
content: "짧은 응원 내용입니다.",
27+
},
28+
{
29+
author: "감구마",
30+
content: "짧은 응원 내용입니다.",
31+
},
32+
{
33+
author: "바나나",
34+
content:
35+
"중간 길이의 응원 메시지입니다. 이 가게 음식이 정말 맛있어서 자주 방문하고 있어요. 사장님도 너무 친절하셔서 기분 좋게 식사할 수 있습니다.",
36+
},
37+
{
38+
author: "딸기",
39+
content: "여기 진짜 맛집이에요! 강추합니다.",
40+
},
41+
{
42+
author: "수박",
43+
content:
44+
"이 가게는 제가 가장 좋아하는 곳 중 하나입니다. 음식 맛도 훌륭하고 분위기도 좋아서 친구들과 자주 오곤 해요. 특히 이곳의 대표 메뉴는 정말 일품이라고 생각합니다. 앞으로도 계속 번창하시길 바라며, 더 많은 사람들이 이 맛집을 알게 되었으면 좋겠어요!",
45+
},
46+
{
47+
author: "포도",
48+
content: "맛있어요!",
49+
},
50+
];
51+
52+
export const StoreCheers = ({ storeId }: { storeId: string }) => {
53+
void storeId;
54+
55+
const [visibleCount, setVisibleCount] = useState(3);
56+
57+
const ITEMS_PER_PAGE = 3;
58+
const totalItems = MOCK_CHEER_CARDS.length;
59+
const isAllVisible = visibleCount >= totalItems;
60+
61+
const handleToggle = () => {
62+
if (isAllVisible) {
63+
// 접기: 처음 3개만 보여주기
64+
setVisibleCount(ITEMS_PER_PAGE);
65+
} else {
66+
// 더보기: 3개씩 추가
67+
setVisibleCount(prev => Math.min(prev + ITEMS_PER_PAGE, totalItems));
68+
}
69+
};
70+
71+
const visibleCards = MOCK_CHEER_CARDS.slice(0, visibleCount);
72+
73+
return (
74+
<VStack gap={16} className={styles.storeCheersContainer}>
75+
<Text as='h3' typo='title2Sb' color='text.normal'>
76+
가게에 담긴 응원
77+
</Text>
78+
79+
<VStack>
80+
<VStack gap={24}>
81+
{visibleCards.map(card => (
82+
<CheerCard
83+
key={card.author}
84+
author={card.author}
85+
content={card.content}
86+
/>
87+
))}
88+
</VStack>
89+
<Spacer size={20} />
90+
<Button
91+
variant='assistive'
92+
size='large'
93+
fullWidth
94+
onClick={handleToggle}
95+
>
96+
{isAllVisible ? "접기" : "더보기"}
97+
</Button>
98+
<Spacer size={12} />
99+
100+
{/* TODO: 가게 응원하기 버튼 클릭 시 가게 응원 페이지로 이동 */}
101+
<Link href={""}>
102+
<Button variant='primary' size='large' fullWidth>
103+
가게 응원하기
104+
</Button>
105+
</Link>
106+
</VStack>
107+
</VStack>
108+
);
109+
};
110+
111+
const CheerCard = ({
112+
author,
113+
content,
114+
}: {
115+
author: string;
116+
content: string;
117+
}) => {
118+
const [isExpanded, setIsExpanded] = useState(false);
119+
120+
const isLongText = content.length > 50;
121+
122+
return (
123+
<VStack gap={12}>
124+
<HStack align='center' gap={8}>
125+
<span className={styles.cheerCardProfileImage}>아이콘</span>
126+
<Text as='span' typo='body1Sb' color='text.normal'>
127+
{author}
128+
</Text>
129+
</HStack>
130+
131+
<HStack align='stretch'>
132+
<hr className={styles.cheerCardDivider} />
133+
<VStack className={styles.cheerCardContent} gap={4} align='start'>
134+
<Text
135+
as='p'
136+
typo='body2Rg'
137+
color='text.normal'
138+
className={styles.cheerCardContentText}
139+
data-expanded={isExpanded}
140+
data-long-text={isLongText}
141+
>
142+
{content}
143+
</Text>
144+
{isLongText && (
145+
<TextButton
146+
size='small'
147+
variant='assistive'
148+
onClick={() => setIsExpanded(!isExpanded)}
149+
>
150+
{isExpanded ? "접기" : "더보기"}
151+
</TextButton>
152+
)}
153+
</VStack>
154+
</HStack>
155+
</VStack>
156+
);
157+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { StoreCheers } from "./StoreCheers";
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { style } from "@vanilla-extract/css";
2+
3+
import { semantic } from "@/styles";
4+
5+
export const storeInfoContentContainer = style({
6+
paddingBlock: "2.4rem",
7+
});
8+
9+
export const divider = style({
10+
width: "0.1rem",
11+
height: "1.2rem",
12+
flexShrink: 0,
13+
backgroundColor: semantic.border.gray,
14+
});
15+
16+
export const kakaoMapButton = style({
17+
color: semantic.text.alternative,
18+
});
19+
20+
export const storeInfoImageCarousel = style({
21+
display: "flex",
22+
gap: "1.2rem",
23+
overflowX: "auto",
24+
scrollbarWidth: "none",
25+
paddingInline: "2rem",
26+
27+
selectors: {
28+
"&::-webkit-scrollbar": {
29+
display: "none",
30+
},
31+
},
32+
});
33+
34+
export const storeInfoImage = style({
35+
width: "calc(100% - 2rem)",
36+
height: "23.9rem",
37+
aspectRatio: 1.35,
38+
flexShrink: 0,
39+
borderRadius: "2.4rem",
40+
objectFit: "cover",
41+
objectPosition: "center",
42+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
5+
import { Bleed } from "@/components/ui/Bleed";
6+
import { HStack, VStack } from "@/components/ui/Stack";
7+
import { Text } from "@/components/ui/Text";
8+
import { TextButton } from "@/components/ui/TextButton";
9+
10+
import * as styles from "./StoreInfo.css";
11+
12+
const MOCK_STORE_INFO = {
13+
name: "댈러스피자",
14+
address: "서울 영등포구",
15+
category: "양식",
16+
kakaoMapUrl: "https://place.map.kakao.com/974847893",
17+
};
18+
19+
const MOCK_STORE_IMAGES = [
20+
"https://picsum.photos/200/300",
21+
"https://picsum.photos/200/300",
22+
"https://picsum.photos/200/300",
23+
];
24+
25+
export const StoreInfo = ({ storeId }: { storeId: string }) => {
26+
void storeId;
27+
28+
return (
29+
<VStack>
30+
<StoreInfoImageCarousel images={MOCK_STORE_IMAGES} />
31+
<StoreInfoContent
32+
name={MOCK_STORE_INFO.name}
33+
address={MOCK_STORE_INFO.address}
34+
category={MOCK_STORE_INFO.category}
35+
kakaoMapUrl={MOCK_STORE_INFO.kakaoMapUrl}
36+
/>
37+
</VStack>
38+
);
39+
};
40+
41+
const StoreInfoImageCarousel = ({ images }: { images: string[] }) => {
42+
return (
43+
<Bleed inline={20}>
44+
<div className={styles.storeInfoImageCarousel}>
45+
{images.map((image, index) => (
46+
// TODO: NextImage 사용, priority 1~3 high
47+
<img
48+
key={index}
49+
className={styles.storeInfoImage}
50+
src={image}
51+
alt={`${index + 1}번째 가게 이미지`}
52+
/>
53+
))}
54+
</div>
55+
</Bleed>
56+
);
57+
};
58+
59+
const StoreInfoContent = ({
60+
name,
61+
address,
62+
category,
63+
kakaoMapUrl,
64+
}: {
65+
name: string;
66+
address: string;
67+
category: string;
68+
kakaoMapUrl: string;
69+
}) => {
70+
return (
71+
<VStack gap={16} className={styles.storeInfoContentContainer}>
72+
<VStack gap={4}>
73+
<Text as='span' typo='body1Md' color='text.alternative'>
74+
잇다가 응원하는
75+
</Text>
76+
<Text as='span' typo='title1Bd' color='text.normal'>
77+
{name}
78+
</Text>
79+
</VStack>
80+
81+
<VStack gap={4} align='start' style={{ paddingInline: "0.8rem" }}>
82+
<HStack gap={4} align='center'>
83+
<span>아이콘</span>
84+
<Text as='span' typo='label1Md' color='text.alternative'>
85+
{address}
86+
</Text>
87+
<hr className={styles.divider} />
88+
<Text as='span' typo='label1Md' color='text.alternative'>
89+
{category}
90+
</Text>
91+
</HStack>
92+
<Link href={kakaoMapUrl} target='_blank' rel='noopener noreferrer'>
93+
<TextButton
94+
size='small'
95+
variant='custom'
96+
className={styles.kakaoMapButton}
97+
leftAddon={<span>맵 아이콘</span>}
98+
>
99+
카카오맵 바로가기
100+
</TextButton>
101+
</Link>
102+
</VStack>
103+
</VStack>
104+
);
105+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { StoreInfo } from "./StoreInfo";
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { style } from "@vanilla-extract/css";
2+
3+
// StoreStories 컴포넌트 스타일
4+
export const storeStoriesContainer = style({
5+
paddingTop: "2.8rem",
6+
paddingBottom: "4rem",
7+
});

0 commit comments

Comments
 (0)