From ed7a33d57737bcf82c9f71f7086ed0e190d56c5e Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 15 Sep 2026 15:34:34 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C/=EB=8F=84=EC=B0=A9=20=EC=8B=9C=20=EC=95=88=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EB=A9=94=EC=84=B8=EC=A7=80=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=EC=97=90=EC=84=9C=20=EB=B0=94=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=B8=EB=8A=94=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/notification/historyCache.ts | 92 ++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/utils/notification/historyCache.ts diff --git a/src/utils/notification/historyCache.ts b/src/utils/notification/historyCache.ts new file mode 100644 index 00000000..a006fc20 --- /dev/null +++ b/src/utils/notification/historyCache.ts @@ -0,0 +1,92 @@ +import type { InfiniteData } from "@tanstack/react-query"; + +import type { + INotificationHistoryData, + INotificationHistoryItem, +} from "@/types/notification/notification"; + +export type TNotificationHistoryCache = InfiniteData< + INotificationHistoryData, + string | null +>; + +export function emptyHistoryCache(): TNotificationHistoryCache { + return { pages: [], pageParams: [] }; +} + +//알림 단건 읽음 처리 +export function markNotificationRead( + cache: TNotificationHistoryCache | undefined, + userNotificationId: number, +): TNotificationHistoryCache { + if (!cache) return emptyHistoryCache(); + + return { + ...cache, + pages: cache.pages.map((page) => ({ + ...page, + notifications: page.notifications.map((item) => + item.userNotificationId === userNotificationId + ? { ...item, isRead: true } + : item, + ), + })), + }; +} + +//모두 읽음 처리 +export function markAllNotificationRead( + cache: TNotificationHistoryCache | undefined, +): TNotificationHistoryCache { + if (!cache) return emptyHistoryCache(); + + return { + ...cache, + pages: cache.pages.map((page) => ({ + ...page, + notifications: page.notifications.map((item) => ({ + ...item, + isRead: true, + })), + })), + }; +} + +//새알림 맨 위로 붙이기 +export function prependNotification( + cache: TNotificationHistoryCache | undefined, + item: INotificationHistoryItem, +): TNotificationHistoryCache { + // 캐시가 없거나 페이지 0장이면 새로운 페이지 만들어서 알림 추가 + if (!cache || cache.pages.length === 0) { + return { + pages: [{ hasNext: false, nextCursor: null, notifications: [item] }], + pageParams: [null], + }; + } + + // 이미 페이지가 있으면 페이지 맨위에 알림 추가 + const alreadyExists = cache.pages.some((page) => + page.notifications.some( + (n) => n.userNotificationId === item.userNotificationId, + ), + ); + // 이미 있는 알림이면 캐시 그대로 반환 + if (alreadyExists) return cache; + + //firstPage = 1페이지 (최신알람들 있는곳) + //restPages = 2페이지부터 나머지 다 + const [firstPage, ...restPages] = cache.pages; + const [firstParam, ...restParams] = cache.pageParams; + + return { + pages: [ + { + ...firstPage, + notifications: [item, ...firstPage.notifications], //[새알림, ...예전1페이지알림들] + }, + ...restPages, + ], + pageParams: [firstParam, ...restParams], + }; +} From 0a50ca956616d0f8e4cc69d8aa8c926c9088c937 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 15 Sep 2026 15:57:38 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=EC=88=98=20=EC=97=85=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20payload=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/notification/push.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/types/notification/push.ts b/src/types/notification/push.ts index 62d6b4dd..b75bfd36 100644 --- a/src/types/notification/push.ts +++ b/src/types/notification/push.ts @@ -1,3 +1,5 @@ +import type { TNotificationType } from "@/types/notification/notification"; + export interface IVapidPublicKeyData { publicKey: string; } @@ -21,3 +23,20 @@ export interface IPushSubscriptionRequest { export interface IDeletePushSubscriptionReqest { endpoint: string; } + +// payload를 받았을때, 앱도 어떤 거 왔는지 알게 한 뒤, 앱에서 처리할 수 있도록 하는 인터페이스. +// 기존에는 payload 왔다만 알고, 앱에서는 처리할 수 없어서 실시간 알림+1을 처리할 수 없었음. +// 그래서 payload를 받았을때, 브라우저도 어떤 알림이 왔는지 알 수 있도록 처리. +export interface IPushReceivedPayload { + title?: string; + body?: string; + message?: string; + type?: TNotificationType; + userNotificationId?: number; + orgId?: number; +} + +export interface IPushReceivedMessage { + type: "PUSH_RECEIVED"; + payload?: IPushReceivedPayload; +} From a41eb287cebbcccd5b0b03f6d3a03f947677ac82 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 15 Sep 2026 16:27:47 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=EC=9D=BD=EC=9D=8C/=EB=AA=A8=EB=91=90=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20optimistic=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/notification/useNotificationRead.ts | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/hooks/notification/useNotificationRead.ts b/src/hooks/notification/useNotificationRead.ts index c516d1f2..d1cfdfc1 100644 --- a/src/hooks/notification/useNotificationRead.ts +++ b/src/hooks/notification/useNotificationRead.ts @@ -3,6 +3,12 @@ import { toast } from "sonner"; import type { IApiErrorResponse } from "@/types/common/common"; import type { TNotificationEmptyData } from "@/types/setting/notification"; +import { + markAllNotificationRead, + markNotificationRead, + type TNotificationHistoryCache, +} from "@/utils/notification/historyCache"; + import { useCoreMutation } from "@/hooks/customQuery"; import { @@ -15,7 +21,13 @@ import useWorkspaceStore from "@/store/useWorkspaceStore"; export function useReadNotification() { const orgId = useWorkspaceStore((s) => s.selectedOrgId); - return useCoreMutation( + return useCoreMutation< + TNotificationEmptyData, + number, + IApiErrorResponse, + { prevData?: unknown }, + TNotificationHistoryCache + >( (useNotificationId: number) => { if (orgId == null) { return Promise.reject(new Error("워크스페이스를 선택해주세요")); @@ -23,6 +35,20 @@ export function useReadNotification() { return readNotificationHistory(orgId, useNotificationId); }, { + // 작동예시 + // mutate(5) 호출 + // 아직 서버 응답 전 updateFn이 캐시 id 5번을 isRead: true로 변경 -> 바로 뱃지 -1 + // 동시에 PATCH 요청 + // 실패하면 4번칸의 prevData로 캐시 복구 -> 뱃지 +1 (즉,원상복구) + // 성공하면 history 다시 GET해서 서버랑 맞춤 + optimisticUpdate: + orgId != null + ? { + key: QUERY_KEYS.notification.history(orgId), + updateFn: (old, userNotificationId) => + markNotificationRead(old, userNotificationId), + } + : undefined, invalidateKeys: orgId != null ? [QUERY_KEYS.notification.history(orgId)] : [], userOnError: (error) => { @@ -38,7 +64,13 @@ export function useReadNotification() { export function useAllReadNotifications() { const orgId = useWorkspaceStore((s) => s.selectedOrgId); - return useCoreMutation( + return useCoreMutation< + TNotificationEmptyData, + void, + IApiErrorResponse, + { prevData?: unknown }, + TNotificationHistoryCache + >( () => { if (orgId == null) { return Promise.reject(new Error("워크스페이스를 선택해주세요")); @@ -46,6 +78,13 @@ export function useAllReadNotifications() { return readAllNotificationHistory(orgId); }, { + optimisticUpdate: + orgId != null + ? { + key: QUERY_KEYS.notification.history(orgId), + updateFn: (old) => markAllNotificationRead(old), + } + : undefined, invalidateKeys: orgId != null ? [QUERY_KEYS.notification.history(orgId)] : [], userOnError: (error) => { From e43c3c3c60f980a4dfa590f38290f614f192b9a4 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 15 Sep 2026 16:44:34 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=ED=91=B8=EC=8B=9C=20payload?= =?UTF-8?q?=EB=A1=9C=20=EC=95=8C=EB=A6=BC=20=EB=AA=A9=EB=A1=9D=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw.js | 2 +- .../usePushNotificationRuntime.ts | 46 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/public/sw.js b/public/sw.js index eda53e11..07bcf599 100644 --- a/public/sw.js +++ b/public/sw.js @@ -24,7 +24,7 @@ self.addEventListener("push", (event) => { includeUncontrolled: true, }); clients.forEach((client) => { - client.postMessage({ type: "PUSH_RECEIVED" }); + client.postMessage({ type: "PUSH_RECEIVED" }, payload); }); })(), ); diff --git a/src/hooks/notification/usePushNotificationRuntime.ts b/src/hooks/notification/usePushNotificationRuntime.ts index 72b0d6e6..36758c5e 100644 --- a/src/hooks/notification/usePushNotificationRuntime.ts +++ b/src/hooks/notification/usePushNotificationRuntime.ts @@ -1,6 +1,13 @@ import { useEffect } from "react"; import { useQueryClient } from "@tanstack/react-query"; +import type { TNotificationType } from "@/types/notification/notification"; +import type { IPushReceivedMessage } from "@/types/notification/push"; + +import { + prependNotification, + type TNotificationHistoryCache, +} from "@/utils/notification/historyCache"; import { registerPushServiceWorker, syncPushSubscription, @@ -8,8 +15,25 @@ import { import { useMyNotificationSettings } from "@/hooks/setting/useMyNotificationSettings"; +import { QUERY_KEYS } from "@/lib/queryKeys"; import useWorkspaceStore from "@/store/useWorkspaceStore"; +const NOTIFICATION_TYPES: TNotificationType[] = [ + "BOT_CLICKS", + "CLICKS_INCREASE", + "REPORT", +]; + +function toNotificationType(value: unknown): TNotificationType { + if ( + typeof value === "string" && + NOTIFICATION_TYPES.includes(value as TNotificationType) + ) { + return value as TNotificationType; + } + return "REPORT"; +} + export function usePushNotificationRuntime() { const orgId = useWorkspaceStore((s) => s.selectedOrgId); const { data: settings } = useMyNotificationSettings(); @@ -23,10 +47,26 @@ export function usePushNotificationRuntime() { // 이 브라우저 객체에 serviceWorker라는 기능이 없으면 그만둠 / 있으면 이 브라우저는 SW 지원 if (!("serviceWorker" in navigator)) return; - const onMessage = (event: MessageEvent) => { + const onMessage = (event: MessageEvent) => { if (event.data?.type !== "PUSH_RECEIVED") return; + if (orgId == null) return; + + const payload = event.data.payload; + if (payload?.orgId != null && payload.orgId !== orgId) return; + + const historyKey = QUERY_KEYS.notification.history(orgId); + queryClient.setQueryData(historyKey, (old) => + prependNotification(old, { + userNotificationId: payload?.userNotificationId ?? -Date.now(), + title: payload?.title ?? "알림", + message: payload?.message ?? payload?.body ?? "", + createdAt: new Date().toISOString(), + type: toNotificationType(payload?.type), + isRead: false, + }), + ); void queryClient.invalidateQueries({ - queryKey: ["notification", "history"], + queryKey: historyKey, }); }; @@ -35,7 +75,7 @@ export function usePushNotificationRuntime() { return () => { navigator.serviceWorker.removeEventListener("message", onMessage); }; - }, [queryClient]); + }, [orgId, queryClient]); useEffect(() => { if (orgId == null) return; From 0ce739b14a340d0a5d5869b83fca8475d99ff161 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 15 Sep 2026 17:03:26 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=ED=91=B8=EC=8B=9C=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=EB=A5=BC=20payload=EC=99=80=20=EA=B0=99?= =?UTF-8?q?=EC=9D=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw.js | 2 +- .../__tests__/historyCache.test.ts | 81 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/utils/notification/__tests__/historyCache.test.ts diff --git a/public/sw.js b/public/sw.js index 07bcf599..ac7984e8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -24,7 +24,7 @@ self.addEventListener("push", (event) => { includeUncontrolled: true, }); clients.forEach((client) => { - client.postMessage({ type: "PUSH_RECEIVED" }, payload); + client.postMessage({ type: "PUSH_RECEIVED", payload }); }); })(), ); diff --git a/src/utils/notification/__tests__/historyCache.test.ts b/src/utils/notification/__tests__/historyCache.test.ts new file mode 100644 index 00000000..f6e3145f --- /dev/null +++ b/src/utils/notification/__tests__/historyCache.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; + +import type { INotificationHistoryItem } from "@/types/notification/notification"; + +import { + markAllNotificationRead, + markNotificationRead, + prependNotification, + type TNotificationHistoryCache, +} from "@/utils/notification/historyCache"; + +const item = (id: number, isRead: boolean): INotificationHistoryItem => ({ + userNotificationId: id, + title: `알림 ${id}`, + message: "메시지", + createdAt: "2026-09-15T00:00:00.000Z", + type: "REPORT", + isRead, +}); + +const cacheWith = ( + notifications: INotificationHistoryItem[], +): TNotificationHistoryCache => ({ + pages: [{ hasNext: false, nextCursor: null, notifications }], + pageParams: [null], +}); + +describe("historyCache", () => { + describe("markNotificationRead", () => { + it("해당 알림만 읽음 처리한다", () => { + const next = markNotificationRead( + cacheWith([item(1, false), item(2, false)]), + 1, + ); + expect(next.pages[0]?.notifications.map((n) => n.isRead)).toEqual([ + true, + false, + ]); + }); + + it("캐시가 없으면 빈 캐시를 반환한다", () => { + expect(markNotificationRead(undefined, 1)).toEqual({ + pages: [], + pageParams: [], + }); + }); + }); + + describe("markAllNotificationRead", () => { + it("모든 알림을 읽음 처리한다", () => { + const next = markAllNotificationRead( + cacheWith([item(1, false), item(2, false)]), + ); + expect(next.pages[0]?.notifications.every((n) => n.isRead)).toBe(true); + }); + }); + + describe("prependNotification", () => { + it("빈 캐시에도 안읽음 알림을 넣어 뱃지가 바로 +1 될 수 있게 한다", () => { + const next = prependNotification(undefined, item(9, false)); + expect(next.pages[0]?.notifications).toEqual([item(9, false)]); + expect(next.pageParams).toEqual([null]); + }); + + it("첫 페이지 맨 앞에 새 알림을 붙인다", () => { + const next = prependNotification( + cacheWith([item(1, true)]), + item(2, false), + ); + expect( + next.pages[0]?.notifications.map((n) => n.userNotificationId), + ).toEqual([2, 1]); + }); + + it("같은 id면 중복으로 붙이지 않는다", () => { + const existing = cacheWith([item(1, false)]); + const next = prependNotification(existing, item(1, false)); + expect(next.pages[0]?.notifications).toHaveLength(1); + }); + }); +}); From 2c89c078c1e0218f7b11403ea9e9fe8e7a36224d Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 15 Sep 2026 17:04:13 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/historyCache.test.ts | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 src/utils/notification/__tests__/historyCache.test.ts diff --git a/src/utils/notification/__tests__/historyCache.test.ts b/src/utils/notification/__tests__/historyCache.test.ts deleted file mode 100644 index f6e3145f..00000000 --- a/src/utils/notification/__tests__/historyCache.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import type { INotificationHistoryItem } from "@/types/notification/notification"; - -import { - markAllNotificationRead, - markNotificationRead, - prependNotification, - type TNotificationHistoryCache, -} from "@/utils/notification/historyCache"; - -const item = (id: number, isRead: boolean): INotificationHistoryItem => ({ - userNotificationId: id, - title: `알림 ${id}`, - message: "메시지", - createdAt: "2026-09-15T00:00:00.000Z", - type: "REPORT", - isRead, -}); - -const cacheWith = ( - notifications: INotificationHistoryItem[], -): TNotificationHistoryCache => ({ - pages: [{ hasNext: false, nextCursor: null, notifications }], - pageParams: [null], -}); - -describe("historyCache", () => { - describe("markNotificationRead", () => { - it("해당 알림만 읽음 처리한다", () => { - const next = markNotificationRead( - cacheWith([item(1, false), item(2, false)]), - 1, - ); - expect(next.pages[0]?.notifications.map((n) => n.isRead)).toEqual([ - true, - false, - ]); - }); - - it("캐시가 없으면 빈 캐시를 반환한다", () => { - expect(markNotificationRead(undefined, 1)).toEqual({ - pages: [], - pageParams: [], - }); - }); - }); - - describe("markAllNotificationRead", () => { - it("모든 알림을 읽음 처리한다", () => { - const next = markAllNotificationRead( - cacheWith([item(1, false), item(2, false)]), - ); - expect(next.pages[0]?.notifications.every((n) => n.isRead)).toBe(true); - }); - }); - - describe("prependNotification", () => { - it("빈 캐시에도 안읽음 알림을 넣어 뱃지가 바로 +1 될 수 있게 한다", () => { - const next = prependNotification(undefined, item(9, false)); - expect(next.pages[0]?.notifications).toEqual([item(9, false)]); - expect(next.pageParams).toEqual([null]); - }); - - it("첫 페이지 맨 앞에 새 알림을 붙인다", () => { - const next = prependNotification( - cacheWith([item(1, true)]), - item(2, false), - ); - expect( - next.pages[0]?.notifications.map((n) => n.userNotificationId), - ).toEqual([2, 1]); - }); - - it("같은 id면 중복으로 붙이지 않는다", () => { - const existing = cacheWith([item(1, false)]); - const next = prependNotification(existing, item(1, false)); - expect(next.pages[0]?.notifications).toHaveLength(1); - }); - }); -}); From 96f7e66879546a2b0e4c2a63daadf628f7508f6e Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 16 Sep 2026 16:30:20 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EB=82=A1=EC=9D=80=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=EC=BD=9C=EB=B0=B1=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/customQuery.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/hooks/customQuery.ts b/src/hooks/customQuery.ts index 13df81a5..c8b5cb8b 100644 --- a/src/hooks/customQuery.ts +++ b/src/hooks/customQuery.ts @@ -91,11 +91,15 @@ export function useCoreMutation< }, onError: (error, vars, ctx) => { - if (optimisticUpdate && ctx?.prevData !== undefined) { - queryClient.setQueryData( - optimisticUpdate.key, - ctx.prevData as TCache, - ); + if (optimisticUpdate) { + // setQueryData로 캐시를 복구하는 대신, 퀴리를 취소하여 이전 데이터를 유지하도록 함 + void queryClient.cancelQueries({ queryKey: optimisticUpdate.key }); + } + // invalidateKeys가 존재하면 해당 키들을 무효화하여 데이터를 다시 가져오도록 함 + if (invalidateKeys?.length) { + invalidateKeys.forEach((key) => { + void queryClient.invalidateQueries({ queryKey: key }); + }); } userOnError?.(error, vars, ctx); }, From 7081ecd2698042859dcdb071f70dc75809405916 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 16 Sep 2026 17:32:24 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EC=95=88=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=88=98=EB=A5=BC=20=EC=A6=89=EC=8B=9C=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/customQuery.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hooks/customQuery.ts b/src/hooks/customQuery.ts index c8b5cb8b..558fb81f 100644 --- a/src/hooks/customQuery.ts +++ b/src/hooks/customQuery.ts @@ -91,9 +91,8 @@ export function useCoreMutation< }, onError: (error, vars, ctx) => { - if (optimisticUpdate) { - // setQueryData로 캐시를 복구하는 대신, 퀴리를 취소하여 이전 데이터를 유지하도록 함 - void queryClient.cancelQueries({ queryKey: optimisticUpdate.key }); + if (optimisticUpdate && ctx?.prevData !== undefined) { + queryClient.setQueryData(optimisticUpdate.key, ctx?.prevData); } // invalidateKeys가 존재하면 해당 키들을 무효화하여 데이터를 다시 가져오도록 함 if (invalidateKeys?.length) {