Skip to content

Commit d0779ed

Browse files
authored
Merge pull request #545 from solid-connection/feat/community-ssr-hydration
feat: 커뮤니티 목록 SSR 하이드레이션 적용
2 parents 0c5ce49 + 767f08b commit d0779ed

13 files changed

Lines changed: 144 additions & 25 deletions

File tree

apps/web/src/apis/community/api.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ import type {
1212
} from "@/types/community";
1313
import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance";
1414

15-
// QueryKeys for community domain
16-
export const CommunityQueryKeys = {
17-
posts: "posts",
18-
postList: "postList1", // 기존 api/boards와 동일한 키 유지
19-
} as const;
15+
export { CommunityQueryKeys } from "./queryKeys";
2016

2117
export interface BoardListResponse {
2218
0: string;

apps/web/src/apis/community/deletePost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const useDeletePost = () => {
4747
// 'posts' 쿼리 키를 가진 모든 쿼리를 무효화하여
4848
// 게시글 목록을 다시 불러오도록 합니다.
4949
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] });
50+
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.postList] });
5051

5152
// ISR 페이지 revalidate
5253
if (variables.boardCode && accessToken) {
Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
1-
import { useQuery } from "@tanstack/react-query";
1+
import { queryOptions, useQuery } from "@tanstack/react-query";
22

3-
import { CommunityQueryKeys, communityApi } from "./api";
3+
import { communityApi } from "./api";
4+
import {
5+
COMMUNITY_POST_LIST_GC_TIME,
6+
COMMUNITY_POST_LIST_STALE_TIME,
7+
communityPostListQueryKey,
8+
sortCommunityPosts,
9+
} from "./postListQuery";
410

511
interface UseGetPostListProps {
612
boardCode: string;
713
category?: string | null;
814
}
915

16+
export const getPostListQueryOptions = ({ boardCode, category = null }: UseGetPostListProps) =>
17+
queryOptions({
18+
queryKey: communityPostListQueryKey(boardCode, category),
19+
queryFn: async () => {
20+
const response = await communityApi.getPostList(boardCode, category);
21+
return sortCommunityPosts(response.data);
22+
},
23+
staleTime: COMMUNITY_POST_LIST_STALE_TIME,
24+
gcTime: COMMUNITY_POST_LIST_GC_TIME,
25+
});
26+
1027
/**
1128
* @description 게시글 목록 조회 훅
1229
*/
1330
const useGetPostList = ({ boardCode, category = null }: UseGetPostListProps) => {
14-
return useQuery({
15-
queryKey: [CommunityQueryKeys.postList, boardCode, category],
16-
queryFn: () => communityApi.getPostList(boardCode, category),
17-
staleTime: Infinity,
18-
gcTime: 1000 * 60 * 30, // 30분
19-
select: (response) => {
20-
return [...response.data].sort((a, b) => {
21-
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
22-
});
23-
},
24-
});
31+
return useQuery(getPostListQueryOptions({ boardCode, category }));
2532
};
2633

2734
export default useGetPostList;

apps/web/src/apis/community/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export { default as useDeletePost } from "./deletePost";
1515
export { default as useGetBoard } from "./getBoard";
1616
export { default as useGetBoardList } from "./getBoardList";
1717
export { default as useGetPostDetail } from "./getPostDetail";
18-
export { default as useGetPostList } from "./getPostList";
18+
export { default as useGetPostList, getPostListQueryOptions } from "./getPostList";
1919
export { default as usePatchUpdateComment } from "./patchUpdateComment";
2020
export { default as useUpdatePost } from "./patchUpdatePost";
2121
export { default as useCreateComment } from "./postCreateComment";

apps/web/src/apis/community/patchUpdatePost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const useUpdatePost = () => {
4545
// 해당 게시글 상세 쿼리와 목록 쿼리를 무효화
4646
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] });
4747
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] });
48+
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.postList] });
4849

4950
// ISR 페이지 revalidate
5051
if (variables.boardCode && accessToken) {

apps/web/src/apis/community/postCreatePost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const useCreatePost = () => {
3838
onSuccess: async (data) => {
3939
// 게시글 목록 쿼리를 무효화하여 최신 목록 반영
4040
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] });
41+
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.postList] });
4142

4243
// ISR 페이지 revalidate (사용자 인증 토큰 사용)
4344
if (accessToken) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ListPost } from "@/types/community";
2+
import { CommunityQueryKeys } from "./queryKeys";
3+
4+
export const COMMUNITY_INITIAL_CATEGORY = "전체";
5+
export const COMMUNITY_POST_LIST_STALE_TIME = Infinity;
6+
export const COMMUNITY_POST_LIST_GC_TIME = 1000 * 60 * 30; // 30분
7+
8+
export const communityPostListQueryKey = (boardCode: string, category: string | null = null) =>
9+
[CommunityQueryKeys.postList, boardCode, category] as const;
10+
11+
export const sortCommunityPosts = (posts: ListPost[]) =>
12+
[...posts].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// QueryKeys for community domain
2+
export const CommunityQueryKeys = {
3+
posts: "posts",
4+
postList: "postList1", // 기존 api/boards와 동일한 키 유지
5+
} as const;

apps/web/src/app/community/[boardCode]/CommunityPageContent.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import { useRouter } from "next/navigation";
44
import { useMemo, useState } from "react";
55
import { useGetPostList } from "@/apis/community";
6+
import { COMMUNITY_INITIAL_CATEGORY } from "@/apis/community/postListQuery";
67
import ButtonTab from "@/components/ui/ButtonTab";
78
import { COMMUNITY_BOARDS, COMMUNITY_CATEGORIES } from "@/constants/community";
89
import useReportedPostsStore from "@/lib/zustand/useReportedPostsStore";
910
import type { ListPost } from "@/types/community";
11+
import { CommunityPostListSkeleton } from "./CommunityPageSkeleton";
1012
import CommunityRegionSelector from "./CommunityRegionSelector";
1113
import PostCards from "./PostCards";
1214
import PostWriteButton from "./PostWriteButton";
@@ -23,11 +25,11 @@ interface CommunityPageContentProps {
2325

2426
const CommunityPageContent = ({ boardCode }: CommunityPageContentProps) => {
2527
const router = useRouter();
26-
const [category, setCategory] = useState<string | null>("전체");
28+
const [category, setCategory] = useState<string | null>(COMMUNITY_INITIAL_CATEGORY);
2729
const reportedPostIds = useReportedPostsStore((state) => state.reportedPostIds);
2830
const blockedUserIds = useReportedPostsStore((state) => state.blockedUserIds);
2931

30-
const { data: posts = [] } = useGetPostList({
32+
const { data: posts = [], isPending } = useGetPostList({
3133
boardCode,
3234
category,
3335
});
@@ -81,7 +83,7 @@ const CommunityPageContent = ({ boardCode }: CommunityPageContentProps) => {
8183
setChoice={setCategory}
8284
style={{ padding: "10px 0 10px 18px" }}
8385
/>
84-
{<PostCards posts={visiblePosts} boardCode={boardCode} />}
86+
{isPending ? <CommunityPostListSkeleton /> : <PostCards posts={visiblePosts} boardCode={boardCode} />}
8587
<PostWriteButton onClick={postWriteHandler} />
8688
</div>
8789
);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const TAB_SKELETON_WIDTHS = ["w-12", "w-12", "w-12"];
2+
3+
export const CommunityPostListSkeleton = ({ itemCount = 5 }: { itemCount?: number }) => (
4+
<div
5+
className="flex flex-col overflow-hidden"
6+
style={{
7+
height: "calc(100vh - 220px)",
8+
}}
9+
aria-hidden="true"
10+
>
11+
{Array.from({ length: itemCount }).map((_, index) => (
12+
<div key={index} className="flex justify-between border-b border-b-gray-c-100 px-5 py-4">
13+
<div className="min-w-0 flex-1 animate-pulse">
14+
<div className="flex items-center gap-2.5">
15+
<div className="h-4 w-9 rounded bg-k-50" />
16+
<div className="h-4 w-20 rounded bg-k-50" />
17+
</div>
18+
<div className="mt-3 h-5 w-3/4 rounded bg-k-50" />
19+
<div className="mt-2 h-4 w-full rounded bg-k-50" />
20+
<div className="mt-1.5 h-4 w-2/3 rounded bg-k-50" />
21+
<div className="mt-3 flex gap-2.5">
22+
<div className="h-4 w-8 rounded bg-k-50" />
23+
<div className="h-4 w-8 rounded bg-k-50" />
24+
</div>
25+
</div>
26+
<div className="ml-4 mt-3 h-20 w-20 shrink-0 animate-pulse rounded border border-k-100 bg-k-50" />
27+
</div>
28+
))}
29+
</div>
30+
);
31+
32+
const CommunityPageSkeleton = () => (
33+
<div role="status" aria-label="커뮤니티 게시글을 불러오는 중입니다">
34+
<div className="pb-3.5 pl-5 pt-5">
35+
<div className="h-7 w-24 animate-pulse rounded bg-k-50" />
36+
</div>
37+
<div className="flex gap-2 overflow-hidden px-[18px] py-2.5">
38+
{TAB_SKELETON_WIDTHS.map((width, index) => (
39+
<div key={`${width}-${index}`} className={`${width} h-8 shrink-0 animate-pulse rounded-full bg-k-50`} />
40+
))}
41+
</div>
42+
<CommunityPostListSkeleton />
43+
</div>
44+
);
45+
46+
export default CommunityPageSkeleton;

0 commit comments

Comments
 (0)