Skip to content

Commit d533c17

Browse files
authored
Merge pull request #129 from YAPP-Github/feature/PRODUCT-240
feat: 스토리 상세 조회 시 좌/우 클릭으로 이전·다음 스토리 전환 (#128)
2 parents 0d8e48e + d316c4c commit d533c17

File tree

5 files changed

+139
-72
lines changed

5 files changed

+139
-72
lines changed

src/app/story/[id]/_components/StoryDetailContent/StoryDetailContent.css.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { style } from "@vanilla-extract/css";
2+
import { recipe } from "@vanilla-extract/recipes";
23

34
import { radius, semantic } from "@/styles";
45

@@ -30,15 +31,34 @@ export const cancelIcon = style({
3031
color: semantic.icon.white,
3132
});
3233

33-
export const imageCard = style({
34+
export const storyImageArea = style({
3435
position: "relative",
36+
flex: 1,
3537
width: "100%",
3638
height: "100%",
37-
flex: 1,
38-
overflow: "hidden",
3939
});
4040

41-
export const imageContent = style({
41+
export const zone = recipe({
42+
base: {
43+
position: "absolute",
44+
top: 0,
45+
width: "50%",
46+
height: "100%",
47+
cursor: "pointer",
48+
zIndex: 5,
49+
},
50+
variants: {
51+
side: {
52+
left: { left: 0 },
53+
right: { right: 0 },
54+
},
55+
},
56+
defaultVariants: {
57+
side: "left",
58+
},
59+
});
60+
61+
export const informationContent = style({
4262
position: "absolute",
4363
bottom: 0,
4464
left: 0,

src/app/story/[id]/_components/StoryDetailContent/StoryDetailContent.tsx

Lines changed: 101 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,38 @@ import { useRouter } from "next/navigation";
88
import { useState } from "react";
99

1010
import { Avatar } from "@/app/member/_components/Avatar";
11+
import { storiesQueryOptions } from "@/app/story/_api";
1112
import CancelIcon from "@/assets/cancel.svg";
1213
import LocationIcon from "@/assets/location.svg";
1314
import MarketFillIcon from "@/assets/market-fill.svg";
1415
import { GNB } from "@/components/ui/GNB";
1516
import { Text } from "@/components/ui/Text";
1617

1718
import { storyDetailQueryOptions } from "../../_api";
19+
import { KAKAO_PLACE_URL, STORIES_LIMIT } from "../../_constants";
1820
import * as styles from "./StoryDetailContent.css";
1921

2022
type StoryDetailContentProps = {
2123
storyId: string;
2224
};
2325

24-
const KAKAO_PLACE_URL = "https://place.map.kakao.com";
25-
2626
export const StoryDetailContent = ({ storyId }: StoryDetailContentProps) => {
2727
const router = useRouter();
2828

2929
const { data: story } = useSuspenseQuery(storyDetailQueryOptions(storyId));
30+
const { data: storiesData } = useSuspenseQuery(
31+
storiesQueryOptions(STORIES_LIMIT)
32+
);
3033

3134
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
3235

36+
const currentStory = storiesData.stories.findIndex(
37+
s => s.storyId.toString() === storyId
38+
);
39+
40+
const hasPrevious = currentStory > 0;
41+
const hasNext = currentStory < storiesData.stories.length - 1;
42+
3343
const handleCancelClick = () => {
3444
router.push("/");
3545
};
@@ -38,6 +48,20 @@ export const StoryDetailContent = ({ storyId }: StoryDetailContentProps) => {
3848
setIsDescriptionExpanded(!isDescriptionExpanded);
3949
};
4050

51+
const handlePrevStory = () => {
52+
if (hasPrevious) {
53+
const prevStory = storiesData.stories[currentStory - 1];
54+
router.push(`/story/${prevStory?.storyId}`);
55+
}
56+
};
57+
58+
const handleNextStory = () => {
59+
if (hasNext) {
60+
const nextStory = storiesData.stories[currentStory + 1];
61+
router.push(`/story/${nextStory?.storyId}`);
62+
}
63+
};
64+
4165
return (
4266
<div className={styles.container}>
4367
<div className={styles.gnbOverlay}>
@@ -56,7 +80,15 @@ export const StoryDetailContent = ({ storyId }: StoryDetailContentProps) => {
5680
/>
5781
</div>
5882

59-
<div className={styles.imageCard}>
83+
<div className={styles.storyImageArea}>
84+
<button
85+
className={styles.zone({ side: "left" })}
86+
onClick={handlePrevStory}
87+
/>
88+
<button
89+
className={styles.zone({ side: "right" })}
90+
onClick={handleNextStory}
91+
/>
6092
<Image
6193
src={story.imageUrl}
6294
alt={`${story.storeName} 스토리`}
@@ -66,79 +98,80 @@ export const StoryDetailContent = ({ storyId }: StoryDetailContentProps) => {
6698
// TODO: 추후 제거
6799
unoptimized
68100
/>
69-
<div className={styles.imageContent}>
70-
<div className={styles.userWrapper}>
71-
<Avatar memberId={story.memberId} />
72-
<Text typo='body1Sb' color='common.100'>
73-
{story.memberNickname}
74-
</Text>
75-
</div>
101+
</div>
76102

77-
{story.description && (
78-
<div className={styles.descriptionContainer}>
79-
<motion.div
80-
className={`${styles.descriptionText} ${
81-
isDescriptionExpanded ? styles.expanded : styles.collapsed
82-
}`}
83-
onClick={handleToggle}
84-
animate={{
85-
height: isDescriptionExpanded ? "auto" : "2.2rem",
86-
}}
87-
transition={{
88-
duration: 0.3,
89-
ease: [0.4, 0.0, 0.2, 1],
90-
}}
91-
style={{
92-
overflow: "hidden",
93-
}}
94-
>
95-
<Text typo='body1Sb' color='common.100'>
96-
{story.description}
97-
</Text>
98-
</motion.div>
99-
</div>
100-
)}
103+
<div className={styles.informationContent}>
104+
<div className={styles.userWrapper}>
105+
<Avatar memberId={story.memberId} />
106+
<Text typo='body1Sb' color='common.100'>
107+
{story.memberNickname}
108+
</Text>
109+
</div>
101110

102-
<div className={styles.tagContainer}>
103-
<div className={styles.tag}>
104-
<MarketFillIcon className={styles.tagIcon} />
105-
{story.storeId ? (
106-
<Link href={`/stores/${story.storeId}`}>
107-
<Text typo='label1Sb' color='common.100'>
108-
{story.storeName}
109-
</Text>
110-
</Link>
111-
) : (
112-
<Text typo='label1Sb' color='common.100'>
113-
{story.storeName}
114-
</Text>
115-
)}
116-
</div>
117-
<a
118-
href={`${KAKAO_PLACE_URL}/${story.storeKakaoId}`}
119-
target='_blank'
120-
rel='noopener noreferrer'
111+
{story.description && (
112+
<div className={styles.descriptionContainer}>
113+
<motion.div
114+
className={`${styles.descriptionText} ${
115+
isDescriptionExpanded ? styles.expanded : styles.collapsed
116+
}`}
117+
onClick={handleToggle}
118+
animate={{
119+
height: isDescriptionExpanded ? "auto" : "2.2rem",
120+
}}
121+
transition={{
122+
duration: 0.3,
123+
ease: [0.4, 0.0, 0.2, 1],
124+
}}
125+
style={{
126+
overflow: "hidden",
127+
}}
121128
>
122-
<div className={styles.tag}>
123-
<LocationIcon className={styles.tagIcon} />
129+
<Text typo='body1Sb' color='common.100'>
130+
{story.description}
131+
</Text>
132+
</motion.div>
133+
</div>
134+
)}
135+
136+
<div className={styles.tagContainer}>
137+
<div className={styles.tag}>
138+
<MarketFillIcon className={styles.tagIcon} />
139+
{story.storeId ? (
140+
<Link href={`/stores/${story.storeId}`}>
124141
<Text typo='label1Sb' color='common.100'>
125-
{story.storeDistrict} {story.storeNeighborhood}
142+
{story.storeName}
126143
</Text>
127-
</div>
128-
</a>
144+
</Link>
145+
) : (
146+
<Text typo='label1Sb' color='common.100'>
147+
{story.storeName}
148+
</Text>
149+
)}
129150
</div>
151+
<a
152+
href={`${KAKAO_PLACE_URL}/${story.storeKakaoId}`}
153+
target='_blank'
154+
rel='noopener noreferrer'
155+
>
156+
<div className={styles.tag}>
157+
<LocationIcon className={styles.tagIcon} />
158+
<Text typo='label1Sb' color='common.100'>
159+
{story.storeDistrict} {story.storeNeighborhood}
160+
</Text>
161+
</div>
162+
</a>
130163
</div>
131-
132-
<AnimatePresence>
133-
<motion.div
134-
className={styles.descriptionOverlay}
135-
initial={{ opacity: 0 }}
136-
animate={{ opacity: 1 }}
137-
exit={{ opacity: 0 }}
138-
transition={{ duration: 0.3 }}
139-
/>
140-
</AnimatePresence>
141164
</div>
165+
166+
<AnimatePresence>
167+
<motion.div
168+
className={styles.descriptionOverlay}
169+
initial={{ opacity: 0 }}
170+
animate={{ opacity: 1 }}
171+
exit={{ opacity: 0 }}
172+
transition={{ duration: 0.3 }}
173+
/>
174+
</AnimatePresence>
142175
</div>
143176
);
144177
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./storyDetail.constants";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const KAKAO_PLACE_URL = "https://place.map.kakao.com";
2+
export const STORIES_LIMIT = 20;

src/app/story/_api/stories.queries.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { queryOptions } from "@tanstack/react-query";
22

3+
import { TIME } from "@/constants";
4+
35
import { getStories, getStoriesByKakaoId } from "./stories.api";
46

57
export const storiesQueryKeys = {
@@ -12,10 +14,19 @@ export const storiesQueryKeys = {
1214
[...storiesQueryKeys.kakaoLists(), kakaoId, { size }] as const,
1315
} as const;
1416

17+
export const CACHE_CONSTANTS = {
18+
STORIES_LIST: {
19+
STALE_TIME: 5 * TIME.MINUTE,
20+
GC_TIME: 10 * TIME.MINUTE,
21+
},
22+
} as const;
23+
1524
export const storiesQueryOptions = (size: number) =>
1625
queryOptions({
1726
queryKey: storiesQueryKeys.lists(),
1827
queryFn: () => getStories(size),
28+
staleTime: CACHE_CONSTANTS.STORIES_LIST.STALE_TIME,
29+
gcTime: CACHE_CONSTANTS.STORIES_LIST.GC_TIME,
1930
});
2031

2132
export const storiesByKakaoIdQueryOptions = (

0 commit comments

Comments
 (0)