-
Notifications
You must be signed in to change notification settings - Fork 3
feat: 커뮤니티 목록 SSR 하이드레이션 적용 #545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,34 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { queryOptions, useQuery } from "@tanstack/react-query"; | ||
|
|
||
| import { CommunityQueryKeys, communityApi } from "./api"; | ||
| import { communityApi } from "./api"; | ||
| import { | ||
| COMMUNITY_POST_LIST_GC_TIME, | ||
| COMMUNITY_POST_LIST_STALE_TIME, | ||
| communityPostListQueryKey, | ||
| sortCommunityPosts, | ||
| } from "./postListQuery"; | ||
|
|
||
| interface UseGetPostListProps { | ||
| boardCode: string; | ||
| category?: string | null; | ||
| } | ||
|
|
||
| export const getPostListQueryOptions = ({ boardCode, category = null }: UseGetPostListProps) => | ||
| queryOptions({ | ||
| queryKey: communityPostListQueryKey(boardCode, category), | ||
| queryFn: async () => { | ||
| const response = await communityApi.getPostList(boardCode, category); | ||
| return sortCommunityPosts(response.data); | ||
| }, | ||
| staleTime: COMMUNITY_POST_LIST_STALE_TIME, | ||
| gcTime: COMMUNITY_POST_LIST_GC_TIME, | ||
| }); | ||
|
|
||
| /** | ||
| * @description 게시글 목록 조회 훅 | ||
| */ | ||
| const useGetPostList = ({ boardCode, category = null }: UseGetPostListProps) => { | ||
| return useQuery({ | ||
| queryKey: [CommunityQueryKeys.postList, boardCode, category], | ||
| queryFn: () => communityApi.getPostList(boardCode, category), | ||
| staleTime: Infinity, | ||
| gcTime: 1000 * 60 * 30, // 30분 | ||
| select: (response) => { | ||
| return [...response.data].sort((a, b) => { | ||
| return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); | ||
| }); | ||
| }, | ||
| }); | ||
| return useQuery(getPostListQueryOptions({ boardCode, category })); | ||
| }; | ||
|
|
||
| export default useGetPostList; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import type { ListPost } from "@/types/community"; | ||
| import { CommunityQueryKeys } from "./queryKeys"; | ||
|
|
||
| export const COMMUNITY_INITIAL_CATEGORY = "전체"; | ||
| export const COMMUNITY_POST_LIST_STALE_TIME = Infinity; | ||
| export const COMMUNITY_POST_LIST_GC_TIME = 1000 * 60 * 30; // 30분 | ||
|
|
||
| export const communityPostListQueryKey = (boardCode: string, category: string | null = null) => | ||
| [CommunityQueryKeys.postList, boardCode, category] as const; | ||
|
|
||
| export const sortCommunityPosts = (posts: ListPost[]) => | ||
| [...posts].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // QueryKeys for community domain | ||
| export const CommunityQueryKeys = { | ||
| posts: "posts", | ||
| postList: "postList1", // 기존 api/boards와 동일한 키 유지 | ||
| } as const; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| const TAB_SKELETON_WIDTHS = ["w-12", "w-12", "w-12"]; | ||
|
|
||
| export const CommunityPostListSkeleton = ({ itemCount = 5 }: { itemCount?: number }) => ( | ||
| <div | ||
| className="flex flex-col overflow-hidden" | ||
| style={{ | ||
| height: "calc(100vh - 220px)", | ||
| }} | ||
| aria-hidden="true" | ||
| > | ||
| {Array.from({ length: itemCount }).map((_, index) => ( | ||
| <div key={index} className="flex justify-between border-b border-b-gray-c-100 px-5 py-4"> | ||
| <div className="min-w-0 flex-1 animate-pulse"> | ||
| <div className="flex items-center gap-2.5"> | ||
| <div className="h-4 w-9 rounded bg-k-50" /> | ||
| <div className="h-4 w-20 rounded bg-k-50" /> | ||
| </div> | ||
| <div className="mt-3 h-5 w-3/4 rounded bg-k-50" /> | ||
| <div className="mt-2 h-4 w-full rounded bg-k-50" /> | ||
| <div className="mt-1.5 h-4 w-2/3 rounded bg-k-50" /> | ||
| <div className="mt-3 flex gap-2.5"> | ||
| <div className="h-4 w-8 rounded bg-k-50" /> | ||
| <div className="h-4 w-8 rounded bg-k-50" /> | ||
| </div> | ||
| </div> | ||
| <div className="ml-4 mt-3 h-20 w-20 shrink-0 animate-pulse rounded border border-k-100 bg-k-50" /> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ); | ||
|
|
||
| const CommunityPageSkeleton = () => ( | ||
| <div role="status" aria-label="커뮤니티 게시글을 불러오는 중입니다"> | ||
| <div className="pb-3.5 pl-5 pt-5"> | ||
| <div className="h-7 w-24 animate-pulse rounded bg-k-50" /> | ||
| </div> | ||
| <div className="flex gap-2 overflow-hidden px-[18px] py-2.5"> | ||
| {TAB_SKELETON_WIDTHS.map((width, index) => ( | ||
| <div key={`${width}-${index}`} className={`${width} h-8 shrink-0 animate-pulse rounded-full bg-k-50`} /> | ||
| ))} | ||
| </div> | ||
| <CommunityPostListSkeleton /> | ||
| </div> | ||
| ); | ||
|
|
||
| export default CommunityPageSkeleton; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||||||||||||||
| import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; | ||||||||||||||||||
| import CommunityPageSkeleton from "./CommunityPageSkeleton"; | ||||||||||||||||||
|
|
||||||||||||||||||
| const CommunityLoading = () => ( | ||||||||||||||||||
| <div className="w-full"> | ||||||||||||||||||
| <TopDetailNavigation title="커뮤니티" /> | ||||||||||||||||||
| <CommunityPageSkeleton /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
Comment on lines
+5
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1) 로딩 상태에서 상단 고정 네비게이션과 본문이 겹칠 수 있어요. 수정 예시 const CommunityLoading = () => (
- <div className="w-full">
+ <div className="w-full pt-14">
<TopDetailNavigation title="커뮤니티" />
<CommunityPageSkeleton />
</div>
);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| ); | ||||||||||||||||||
|
|
||||||||||||||||||
| export default CommunityLoading; | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1.
전체와null을 같은 캐시 키로 정규화해 주세요.지금은 같은 “전체 목록” 요청이
["postList1", boardCode, null]와["postList1", boardCode, "전체"]로 나뉩니다.apps/web/src/apis/community/getPostList.tsLine 16은 기본값을null로 두고,apps/web/src/app/community/[boardCode]/page.tsxLines 44-48은"전체"로 프리패치하고 있어서, 다른 호출부가 기본값을 쓰면 하이드레이션된 캐시를 재사용하지 못하고 한 번 더 요청하게 됩니다.🔧 제안 코드
📝 Committable suggestion
🤖 Prompt for AI Agents