Skip to content

Commit d036f12

Browse files
authored
Merge pull request #61 from wafflestudio/32-feature-post-bookmark
게시글 북마크 영역 ui
2 parents 594fe5f + 1c667b6 commit d036f12

File tree

13 files changed

+192
-11
lines changed

13 files changed

+192
-11
lines changed

src/components/post/PostDetail.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect } from 'react'
2-
import { useParams, useNavigate } from '@tanstack/react-router'
2+
import { useParams, useLocation, useNavigate } from '@tanstack/react-router'
33
import { X, ChevronLeft, ChevronRight, Heart } from 'lucide-react'
44
import { motion, AnimatePresence } from 'framer-motion'
55
import PostInfoSection from './PostInfoSection'
@@ -17,6 +17,7 @@ export interface PostData {
1717

1818
export default function PostDetail() {
1919
const { post_id: postId } = useParams({ from: '/_app/p/$post_id' })
20+
const location = useLocation()
2021
const navigate = useNavigate()
2122

2223
const [postData, setPostData] = useState<PostData | null>(null)
@@ -34,7 +35,19 @@ export default function PostDetail() {
3435
}, [postId])
3536

3637
const images = postData?.images || []
37-
const handleClose = () => navigate({ to: '..' })
38+
const handleClose = () => {
39+
const returnToPath = location.search.returnToPath
40+
const returnToSearch = location.search.returnToSearch
41+
42+
if (returnToPath) {
43+
navigate({
44+
to: returnToPath,
45+
search: returnToSearch,
46+
})
47+
} else {
48+
navigate({ to: '/' })
49+
}
50+
}
3851

3952
const moveSlide = (step: number) => {
4053
if (images.length === 0) return
@@ -59,7 +72,7 @@ export default function PostDetail() {
5972

6073
return (
6174
<div
62-
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60"
75+
className="fixed inset-0 z-100 flex items-center justify-center bg-black/60"
6376
onClick={handleClose}
6477
>
6578
<svg width="0" height="0" className="absolute">
@@ -72,7 +85,7 @@ export default function PostDetail() {
7285

7386
<button
7487
onClick={handleClose}
75-
className="absolute top-6 right-6 z-[110] text-white"
88+
className="absolute top-6 right-6 z-110 text-white"
7689
>
7790
<X className="h-10 w-10" />
7891
</button>
@@ -91,7 +104,7 @@ export default function PostDetail() {
91104
{images.map((img, idx) => (
92105
<div
93106
key={idx}
94-
className="flex h-full min-w-full flex-shrink-0 items-center justify-center"
107+
className="flex h-full min-w-full shrink-0 items-center justify-center"
95108
>
96109
<img
97110
src={img}
@@ -122,7 +135,7 @@ export default function PostDetail() {
122135
ease: 'easeInOut',
123136
}}
124137
onAnimationComplete={() => setIsAnimating(false)}
125-
className="pointer-events-none absolute z-[20] flex items-center justify-center"
138+
className="pointer-events-none absolute z-20 flex items-center justify-center"
126139
>
127140
<Heart
128141
className="h-32 w-32 drop-shadow-2xl"

src/entities/feed/ui/FeedList.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { FeedItem } from '@/entities/feed/model/types'
2-
import { useNavigate } from '@tanstack/react-router'
2+
import { useNavigate, useLocation } from '@tanstack/react-router'
33
import { FeedCard } from '@/entities/feed/ui/FeedCard'
44

55
export function FeedList({ items }: { items: FeedItem[] }) {
66
const navigate = useNavigate()
7+
const location = useLocation()
78

89
return (
910
<div className="grid h-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
@@ -15,6 +16,10 @@ export function FeedList({ items }: { items: FeedItem[] }) {
1516
navigate({
1617
to: '/p/$post_id',
1718
params: { post_id: String(postId) },
19+
search: {
20+
returnToPath: location.pathname,
21+
returnToSearch: location.search,
22+
},
1823
})
1924
}
2025
onToggleLike={(postId, liked) => console.log('like', postId, liked)}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { instance } from '@/shared/api/ky'
2+
import {
3+
BookmarkedPostSchema,
4+
ApiResponseSchema,
5+
} from '@/entities/post/model/schema'
6+
import type { BookmarkedPost } from '@/entities/post/model/types'
7+
8+
export async function getBookmarkedPosts(): Promise<BookmarkedPost[]> {
9+
const response = await instance.get('api/v1/posts/bookmarks')
10+
11+
const raw = await response.json()
12+
13+
const parsed = ApiResponseSchema(BookmarkedPostSchema.array()).parse(raw)
14+
15+
if (!parsed.success) {
16+
throw new Error(parsed.message || 'Failed to load bookmarked posts')
17+
}
18+
19+
return parsed.data
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import { getBookmarkedPosts } from '@/entities/post/api/getBookmarkedPosts'
3+
import type { BookmarkedPost } from '@/entities/post/model/types'
4+
5+
export function useBookmarkedPostsQuery() {
6+
return useQuery<BookmarkedPost[]>({
7+
queryKey: ['posts', 'bookmarks'],
8+
queryFn: () => getBookmarkedPosts(),
9+
retry: false,
10+
})
11+
}

src/entities/post/model/schema.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { z } from 'zod'
2+
3+
export const PostImageSchema = z.object({
4+
id: z.number(),
5+
url: z.string(),
6+
orderIndex: z.number(),
7+
})
8+
9+
export const BookmarkedPostSchema = z.object({
10+
id: z.number(),
11+
userId: z.number(),
12+
nickname: z.string(),
13+
profileImageUrl: z.string(),
14+
content: z.string(),
15+
albumId: z.number().nullable(),
16+
images: z.array(PostImageSchema),
17+
likeCount: z.number(),
18+
commentCount: z.number(),
19+
createdAt: z.string(),
20+
updatedAt: z.string(),
21+
liked: z.boolean(),
22+
bookmarked: z.boolean(),
23+
})
24+
25+
export const ApiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
26+
z.object({
27+
code: z.string(),
28+
message: z.string(),
29+
data: dataSchema,
30+
success: z.boolean(),
31+
})

src/entities/post/model/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { z } from 'zod'
2+
import {
3+
BookmarkedPostSchema,
4+
PostImageSchema,
5+
ApiResponseSchema,
6+
} from './schema'
7+
8+
export type PostImage = z.infer<typeof PostImageSchema>
9+
export type BookmarkedPost = z.infer<typeof BookmarkedPostSchema>
10+
11+
export type ApiResponse<T> = z.infer<
12+
ReturnType<typeof ApiResponseSchema<z.ZodType<T>>>
13+
>

src/features/profile-posts/ui/ProfilePostTile.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
11
import { Heart, MessageCircle } from 'lucide-react'
22
import { useMemo, useState } from 'react'
3+
import { useNavigate, useLocation } from '@tanstack/react-router'
34

45
import { cn } from '@/shared/lib/utils'
56
import { ImageFallback } from '@/shared/ui/image-fallback'
67

78
type ProfilePostTileProps = {
89
className?: string
10+
postId: string
911
imageSrc?: string
1012
likeCount: number
1113
commentCount: number
1214
}
1315

1416
export function ProfilePostTile({
1517
className,
18+
postId,
1619
imageSrc,
1720
likeCount,
1821
commentCount,
1922
}: ProfilePostTileProps) {
2023
const [isImageError, setIsImageError] = useState(false)
24+
const navigate = useNavigate()
25+
const location = useLocation()
2126

2227
const countsLabel = useMemo(() => {
2328
return `좋아요 ${likeCount}개, 댓글 ${commentCount}개`
2429
}, [commentCount, likeCount])
2530

31+
const handleClick = () => {
32+
navigate({
33+
to: '/p/$post_id',
34+
params: { post_id: postId },
35+
search: {
36+
returnToPath: location.pathname,
37+
returnToSearch: location.search,
38+
},
39+
})
40+
}
41+
2642
return (
2743
<button
2844
type="button"
@@ -31,6 +47,7 @@ export function ProfilePostTile({
3147
className
3248
)}
3349
aria-label={countsLabel}
50+
onClick={handleClick}
3451
>
3552
{imageSrc && !isImageError ? (
3653
<img

src/features/profile-posts/ui/ProfilePostsGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function ProfilePostsGrid({ className, items }: ProfilePostsGridProps) {
2020
{items.map((item) => (
2121
<ProfilePostTile
2222
key={item.id}
23+
postId={item.id}
2324
imageSrc={item.imageSrc}
2425
likeCount={item.likeCount}
2526
commentCount={item.commentCount}

src/mocks/handlers/post.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,35 @@ export const postHandlers = [
1515
data: post,
1616
})
1717
}),
18+
http.get('*/api/v1/posts/bookmarks', () => {
19+
const bookmarkedPostIds = ['1', '3', '5', '7', '9']
20+
const bookmarkedPosts = posts
21+
.filter((p) => bookmarkedPostIds.includes(p.id))
22+
.map((p, index) => ({
23+
id: Number(p.id),
24+
userId: 1,
25+
nickname: p.username,
26+
profileImageUrl: p.userImage,
27+
content: p.caption,
28+
albumId: null,
29+
images: p.images.map((url, imgIndex) => ({
30+
id: Number(p.id) * 100 + imgIndex,
31+
url,
32+
orderIndex: imgIndex,
33+
})),
34+
likeCount: p.likeCount,
35+
commentCount: p.commentCount,
36+
createdAt: p.createdAt,
37+
updatedAt: p.createdAt,
38+
liked: index % 2 === 0,
39+
bookmarked: true,
40+
}))
41+
42+
return HttpResponse.json({
43+
code: '200',
44+
message: '요청에 성공하였습니다.',
45+
data: bookmarkedPosts,
46+
success: true,
47+
})
48+
}),
1849
]
Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
import { createFileRoute } from '@tanstack/react-router'
22

33
import { ProfileContentContainer } from '@/widgets/profile-layout'
4+
import { ProfilePostsGrid } from '@/features/profile-posts/ui/ProfilePostsGrid'
5+
import { useBookmarkedPostsQuery } from '@/entities/post/model/hooks/useBookmarkedPostsQuery'
6+
import type { ProfilePostGridItem } from '@/features/profile-posts/ui/ProfilePostsGrid'
47

58
export const Route = createFileRoute('/_app/$profile_name/saved')({
69
component: RouteComponent,
710
})
811

912
function RouteComponent() {
13+
const { data: bookmarkedPosts = [], isLoading } = useBookmarkedPostsQuery()
14+
15+
const items: ProfilePostGridItem[] = bookmarkedPosts.map((post) => ({
16+
id: String(post.id),
17+
imageSrc: post.images[0]?.url,
18+
likeCount: post.likeCount,
19+
commentCount: post.commentCount,
20+
}))
21+
22+
if (isLoading) {
23+
return (
24+
<ProfileContentContainer className="py-6">
25+
<div>Loading...</div>
26+
</ProfileContentContainer>
27+
)
28+
}
29+
1030
return (
11-
<ProfileContentContainer className="py-6">저장됨</ProfileContentContainer>
31+
<ProfileContentContainer className="py-6">
32+
<ProfilePostsGrid items={items} />
33+
</ProfileContentContainer>
1234
)
1335
}

0 commit comments

Comments
 (0)