From af206652cb9750365f68c1e77e2ca9915edc533a Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Thu, 9 Oct 2025 15:18:50 +0900 Subject: [PATCH] =?UTF-8?q?4.4=20=EB=82=99=EA=B4=80=EC=A0=81=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=8C=A8=ED=84=B4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9(=EC=9E=91=EC=84=B1/=EC=88=98=EC=A0=95)=20o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/mutations/posts.ts | 81 ++++++++++++++++++++++++++++++++++++++ todolist.txt | 2 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/lib/mutations/posts.ts diff --git a/src/lib/mutations/posts.ts b/src/lib/mutations/posts.ts new file mode 100644 index 0000000..fb85609 --- /dev/null +++ b/src/lib/mutations/posts.ts @@ -0,0 +1,81 @@ +"use client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { fetchJson } from "@/lib/api"; +import { queryKeys } from "@/lib/queryKeys"; + +type CreateCommentInput = { + postId: string; + authorId?: string; + content: string; + isAnonymous?: boolean; + isSecret?: boolean; +}; + +export function useCreateComment(postId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (input: CreateCommentInput) => + fetchJson<{ comment: any }>("/api/comments", { + method: "POST", + body: JSON.stringify(input), + }), + onMutate: async (input) => { + await qc.cancelQueries({ queryKey: queryKeys.comments.list(postId) }); + const previous = qc.getQueryData(queryKeys.comments.list(postId)); + const optimistic = { + id: `temp-${Date.now()}`, + content: input.content, + isAnonymous: !!input.isAnonymous, + isSecret: !!input.isSecret, + createdAt: new Date().toISOString(), + }; + qc.setQueryData(queryKeys.comments.list(postId), (old: any) => ({ + comments: [optimistic, ...(old?.comments ?? [])], + })); + return { previous }; + }, + onError: (_err, _vars, ctx) => { + if (ctx?.previous) qc.setQueryData(queryKeys.comments.list(postId), ctx.previous); + }, + onSettled: () => { + qc.invalidateQueries({ queryKey: queryKeys.comments.list(postId) }); + }, + }); +} + +export function useTogglePin(postId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (vars: { pinned: boolean; order?: number | null; userIdHeader?: string }) => + fetchJson<{ post: { id: string; isPinned: boolean; pinnedOrder: number | null } }>( + `/api/posts/${postId}/pin`, + { + method: "POST", + headers: vars.userIdHeader ? { "x-user-id": vars.userIdHeader } : undefined, + body: JSON.stringify({ pinned: vars.pinned, order: vars.order ?? null }), + } + ), + onMutate: async (vars) => { + // Optimistic update detail cache + await qc.cancelQueries({ queryKey: queryKeys.posts.detail(postId) }); + const previousDetail = qc.getQueryData(queryKeys.posts.detail(postId)); + qc.setQueryData(queryKeys.posts.detail(postId), (old: any) => ({ + ...(old ?? {}), + post: { + ...(old?.post ?? { id: postId }), + isPinned: vars.pinned, + pinnedOrder: vars.pinned ? vars.order ?? 0 : null, + }, + })); + return { previousDetail }; + }, + onError: (_err, _vars, ctx) => { + if (ctx?.previousDetail) qc.setQueryData(queryKeys.posts.detail(postId), ctx.previousDetail); + }, + onSettled: () => { + qc.invalidateQueries({ queryKey: queryKeys.posts.detail(postId) }); + }, + }); +} + + diff --git a/todolist.txt b/todolist.txt index fdd3b61..086c905 100644 --- a/todolist.txt +++ b/todolist.txt @@ -24,7 +24,7 @@ 4.1 React Query 설치 및 Provider 구성 o 4.2 공통 fetcher/에러 형식/재시도·백오프 설정 o 4.3 캐시 키/무효화 전략 수립 및 적용 o -4.4 낙관적 업데이트 패턴 적용(작성/수정) +4.4 낙관적 업데이트 패턴 적용(작성/수정) o [공통/레이아웃] 5.1 앱 레이아웃/헤더/푸터/사이드바 구성