From b520031e38e8009d66e9deafc4e5b231f29d108b Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Tue, 27 Oct 2026 13:56:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=ED=95=A9?= =?UTF-8?q?=EC=B9=A8=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=8F=99=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 - ITEM_PARSING_MERGED 를 알림 타입과 푸시 타입에 추가 - 이동 좌표는 남는 카드라 다른 파싱 알림과 같은 경로로 이동 - 수신 시 파싱 완료와 같은 갱신과 성공 토스트 --- .../web/src/app/notification/_utils/getNotificationRoute.test.ts | 1 + apps/web/src/app/notification/_utils/getNotificationRoute.ts | 1 + apps/web/src/hooks/useNotificationSSE.tsx | 1 + apps/web/src/types/notification.ts | 1 + apps/web/src/utils/pushNotificationRoute.test.ts | 1 + apps/web/src/utils/pushNotificationRoute.ts | 1 + packages/core/src/consts/webBridge.ts | 1 + packages/core/src/types/pushNotification.ts | 1 + 8 files changed, 8 insertions(+) diff --git a/apps/web/src/app/notification/_utils/getNotificationRoute.test.ts b/apps/web/src/app/notification/_utils/getNotificationRoute.test.ts index 14dd4425..236a1fe9 100644 --- a/apps/web/src/app/notification/_utils/getNotificationRoute.test.ts +++ b/apps/web/src/app/notification/_utils/getNotificationRoute.test.ts @@ -9,6 +9,7 @@ const ITEM_TYPES: NotificationTypeT[] = [ 'ITEM_PARSING_INCOMPLETE', 'ITEM_PARSING_FAILED', 'ITEM_PARSING_RECOVERED', + 'ITEM_PARSING_MERGED', 'ITEM_REFRESH_COMPLETED', ]; diff --git a/apps/web/src/app/notification/_utils/getNotificationRoute.ts b/apps/web/src/app/notification/_utils/getNotificationRoute.ts index 76f71ecd..11f4d976 100644 --- a/apps/web/src/app/notification/_utils/getNotificationRoute.ts +++ b/apps/web/src/app/notification/_utils/getNotificationRoute.ts @@ -20,6 +20,7 @@ export const getNotificationRoute = ( case 'ITEM_PARSING_INCOMPLETE': case 'ITEM_PARSING_FAILED': case 'ITEM_PARSING_RECOVERED': + case 'ITEM_PARSING_MERGED': case 'ITEM_REFRESH_COMPLETED': case 'ITEM_REFRESH_FAILED': if (extra?.kind === 'TOURNAMENT' && extra.tournamentId) { diff --git a/apps/web/src/hooks/useNotificationSSE.tsx b/apps/web/src/hooks/useNotificationSSE.tsx index d8bec0da..0db25b64 100644 --- a/apps/web/src/hooks/useNotificationSSE.tsx +++ b/apps/web/src/hooks/useNotificationSSE.tsx @@ -254,6 +254,7 @@ export const useNotificationSSE = (enabled: boolean) => { case 'ITEM_REFRESH_COMPLETED': case 'ITEM_PARSING_COMPLETED': case 'ITEM_PARSING_RECOVERED': + case 'ITEM_PARSING_MERGED': if (payload.kind === 'TOURNAMENT' && payload.tournamentId != null) { queryClient.invalidateQueries({ queryKey: ['tournament', payload.tournamentId], diff --git a/apps/web/src/types/notification.ts b/apps/web/src/types/notification.ts index 30e1fb12..66c3ee66 100644 --- a/apps/web/src/types/notification.ts +++ b/apps/web/src/types/notification.ts @@ -12,6 +12,7 @@ export type NotificationTypeT = | 'ITEM_PARSING_INCOMPLETE' | 'ITEM_PARSING_FAILED' | 'ITEM_PARSING_RECOVERED' + | 'ITEM_PARSING_MERGED' | 'ITEM_REFRESH_COMPLETED' | 'ITEM_REFRESH_FAILED' | 'ANNOUNCEMENT'; diff --git a/apps/web/src/utils/pushNotificationRoute.test.ts b/apps/web/src/utils/pushNotificationRoute.test.ts index 35b7c21f..76f83775 100644 --- a/apps/web/src/utils/pushNotificationRoute.test.ts +++ b/apps/web/src/utils/pushNotificationRoute.test.ts @@ -13,6 +13,7 @@ describe('getPushNotificationRoute', () => { { type: 'ITEM_PARSING_INCOMPLETE', refId: 4, kind: 'TOURNAMENT', tournamentId: 8 }, { type: 'ITEM_PARSING_FAILED', refId: 5, kind: 'TOURNAMENT', tournamentId: 9 }, { type: 'ITEM_PARSING_RECOVERED', refId: 6, kind: 'TOURNAMENT', tournamentId: 10 }, + { type: 'ITEM_PARSING_MERGED', refId: 9, kind: 'WISH' }, { type: 'ITEM_REFRESH_COMPLETED', refId: 7, kind: 'WISH' }, { type: 'ITEM_REFRESH_FAILED', refId: 8, kind: 'WISH' }, ]; diff --git a/apps/web/src/utils/pushNotificationRoute.ts b/apps/web/src/utils/pushNotificationRoute.ts index 1758c283..0fe7e1eb 100644 --- a/apps/web/src/utils/pushNotificationRoute.ts +++ b/apps/web/src/utils/pushNotificationRoute.ts @@ -11,6 +11,7 @@ export const getPushNotificationRoute = (payload: DeepLinkPayloadT) => { case PUSH_NOTIFICATION_TYPE.ITEM_PARSING_INCOMPLETE: case PUSH_NOTIFICATION_TYPE.ITEM_PARSING_FAILED: case PUSH_NOTIFICATION_TYPE.ITEM_PARSING_RECOVERED: + case PUSH_NOTIFICATION_TYPE.ITEM_PARSING_MERGED: case PUSH_NOTIFICATION_TYPE.ITEM_REFRESH_COMPLETED: case PUSH_NOTIFICATION_TYPE.ITEM_REFRESH_FAILED: if (payload.kind === 'TOURNAMENT' && payload.tournamentId) { diff --git a/packages/core/src/consts/webBridge.ts b/packages/core/src/consts/webBridge.ts index 5f88821c..eafeae51 100644 --- a/packages/core/src/consts/webBridge.ts +++ b/packages/core/src/consts/webBridge.ts @@ -66,6 +66,7 @@ export const PUSH_NOTIFICATION_TYPE = { ITEM_PARSING_INCOMPLETE: 'ITEM_PARSING_INCOMPLETE', ITEM_PARSING_FAILED: 'ITEM_PARSING_FAILED', ITEM_PARSING_RECOVERED: 'ITEM_PARSING_RECOVERED', + ITEM_PARSING_MERGED: 'ITEM_PARSING_MERGED', ITEM_REFRESH_COMPLETED: 'ITEM_REFRESH_COMPLETED', ITEM_REFRESH_FAILED: 'ITEM_REFRESH_FAILED', } as const; diff --git a/packages/core/src/types/pushNotification.ts b/packages/core/src/types/pushNotification.ts index af27a0ab..4b30e4a3 100644 --- a/packages/core/src/types/pushNotification.ts +++ b/packages/core/src/types/pushNotification.ts @@ -58,6 +58,7 @@ export type DeepLinkPayloadT = | 'ITEM_PARSING_INCOMPLETE' | 'ITEM_PARSING_FAILED' | 'ITEM_PARSING_RECOVERED' + | 'ITEM_PARSING_MERGED' | 'ITEM_REFRESH_COMPLETED' | 'ITEM_REFRESH_FAILED'; refId: number; From c76061878dca5854e8b55d1b7f013367b6c6416f Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Tue, 27 Oct 2026 14:57:26 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=ED=9B=84=EB=B3=B4=20=ED=95=A9=EC=B9=A8=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EA=B0=B1=EC=8B=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - silent-sync TOURNAMENT_ITEM_MERGED 수신 시 토너먼트와 남는 후보 다시 조회 - 사라진 후보 상세는 다시 조회하지 않음 - 원인: 받아줄 화면 없이 404 만 Sentry 에 쌓임 - 대안: 후보 상세 화면이 404 를 받으면 담기 화면으로 이동 - 기각: 같은 실패 후보를 두 사람이 동시에 볼 때만 생김. 저장·삭제의 404 처리가 이미 담기 화면으로 보냄 --- apps/web/src/hooks/useNotificationSSE.tsx | 10 ++++++++++ apps/web/src/types/notification.ts | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/apps/web/src/hooks/useNotificationSSE.tsx b/apps/web/src/hooks/useNotificationSSE.tsx index 0db25b64..fc1c980a 100644 --- a/apps/web/src/hooks/useNotificationSSE.tsx +++ b/apps/web/src/hooks/useNotificationSSE.tsx @@ -215,6 +215,16 @@ export const useNotificationSSE = (enabled: boolean) => { case 'TOURNAMENT_ITEM_PARSED': queryClient.invalidateQueries({ queryKey: ['tournament', payload.tournamentId] }); break; + case 'TOURNAMENT_ITEM_MERGED': + queryClient.invalidateQueries({ queryKey: ['tournament', payload.tournamentId] }); + queryClient.invalidateQueries({ + queryKey: [ + 'tournamentItem', + payload.tournamentId, + payload.mergedIntoTournamentItemId, + ], + }); + break; case 'UNREAD_COUNT_CHANGED': void queryClient.refetchQueries({ queryKey: QUERY_KEYS.NOTIFICATION.LIST, diff --git a/apps/web/src/types/notification.ts b/apps/web/src/types/notification.ts index 66c3ee66..07bf0a8f 100644 --- a/apps/web/src/types/notification.ts +++ b/apps/web/src/types/notification.ts @@ -53,6 +53,12 @@ export type SilentSyncSsePayloadT = | (typeof ITEM_STATUS)['INCOMPLETE'] | (typeof ITEM_STATUS)['FAILED']; } + | { + type: 'TOURNAMENT_ITEM_MERGED'; + tournamentId: number; + tournamentItemId: number; + mergedIntoTournamentItemId: number; + } | { type: 'UNREAD_COUNT_CHANGED'; unreadCount: number; From f33b7882bbe852ce79e22f4abac0ab8a44ed13cb Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Tue, 27 Oct 2026 14:58:30 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=82=AC=EB=9D=BC=EC=A7=84=20?= =?UTF-8?q?=EC=9C=84=EC=8B=9C=20=EC=88=98=EC=A0=95=C2=B7=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EC=8B=9C=20=EC=9D=B4=EC=A0=84=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위시 삭제 404 시 이전 화면으로 이동 - 이전: 토스트만 노출하고 화면에 남음 - 원인: 합쳐져 사라진 위시는 삭제 성공과 결과가 같음 - 위시 수정 403·404 시 이동을 위시 목록 replace 에서 backWithFallback 으로 교체 - 원인: 삭제 성공과 같이 들어온 화면으로 돌아감 --- apps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.ts | 4 +++- apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.ts b/apps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.ts index 45b9b7ef..0439bf51 100644 --- a/apps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.ts +++ b/apps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.ts @@ -4,7 +4,7 @@ import { toast } from 'sonner'; import { deleteWish } from '@/apis/deleteWish'; import { ROUTES } from '@/consts/route'; import { useBackWithFallback } from '@/hooks/useBackWithFallback'; -import { isGlobalNetError } from '@/utils/apiError'; +import { getApiErrorStatus, isGlobalNetError } from '@/utils/apiError'; import { getApiErrorMessage } from '@/utils/getApiErrorMessage'; /** 위시 단건 삭제 */ @@ -27,6 +27,8 @@ export const useDeleteWish = (wishId: number) => { * 404: 위시 존재하지 않음 */ toast.error(getApiErrorMessage(error)); + + if (getApiErrorStatus(error) === 404) backWithFallback(ROUTES.WISHLIST); }, }); diff --git a/apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts b/apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts index 5b7c6540..878b41b0 100644 --- a/apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts +++ b/apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { ROUTES } from '@/consts/route'; +import { useBackWithFallback } from '@/hooks/useBackWithFallback'; import { getApiErrorStatus, isGlobalNetError } from '@/utils/apiError'; import { getApiErrorMessage } from '@/utils/getApiErrorMessage'; @@ -11,6 +12,7 @@ import type { PatchWishRequestT } from '../_types/wish'; export const usePatchWish = (wishId: number) => { const router = useRouter(); + const backWithFallback = useBackWithFallback(); const queryClient = useQueryClient(); const { mutate: patchWishMutation, isPending: isPatchWishPending } = useMutation({ @@ -39,7 +41,7 @@ export const usePatchWish = (wishId: number) => { * 404: 위시 존재하지 않음 */ const status = getApiErrorStatus(error); - if (status === 403 || status === 404) router.replace(ROUTES.WISHLIST); + if (status === 403 || status === 404) backWithFallback(ROUTES.WISHLIST); }, });