From c84bb4345096f7092cceffd1e5f67ccd871f5e50 Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 4 Jun 2026 13:28:55 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=8C=EB=A6=BC=EB=8B=AB=EA=B8=B0=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/contexts/TradeNotificationContext.tsx | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/frontend/src/contexts/TradeNotificationContext.tsx b/frontend/src/contexts/TradeNotificationContext.tsx index 0713eb3..2ea4948 100644 --- a/frontend/src/contexts/TradeNotificationContext.tsx +++ b/frontend/src/contexts/TradeNotificationContext.tsx @@ -149,6 +149,8 @@ export const TradeNotificationProvider: React.FC = ({ const toastNotificationsRef = useRef(toastNotifications); toastNotificationsRef.current = toastNotifications; const [allNotifications, setAllNotifications] = useState([]); + const allNotificationsRef = useRef(allNotifications); + allNotificationsRef.current = allNotifications; const [detailSignal, setDetailSignal] = useState(null); const [detailCentered, setDetailCentered] = useState(false); @@ -161,11 +163,14 @@ export const TradeNotificationProvider: React.FC = ({ try { const dtos = await loadTradeSignals(); /** - * 항상 DB 캐시의 최신 readIds 사용. - * rawServerList=true 시 빈 Set을 쓰던 기존 코드는 새로고침 후 모든 알림이 - * 미읽음으로 복원되는 버그를 유발했으므로 제거. + * readIds 의 최신 상태를 가져올 때: + * - readIdsRef.current : dismissAllToasts / markAllAsRead 직후 즉시 반영되는 인메모리 값 + * - loadReadIds() : DB 캐시 기반 값 (약간의 지연 가능) + * 두 Set 의 합집합(union)을 사용하면 어느 쪽 타이밍 경쟁에서도 읽음 상태가 보존된다. */ - const read = loadReadIds(); + const cacheRead = loadReadIds(); + const memRead = readIdsRef.current; + const read = new Set([...cacheRead, ...memRead]); const hidden = opts?.rawServerList ? new Set() : loadHiddenIds(); const items = dtos .map(d => dtoToItem(d, read)) @@ -338,35 +343,39 @@ export const TradeNotificationProvider: React.FC = ({ const dismissAllToasts = useCallback(() => { /** - * 전체 닫기 = 토스트 + allNotifications 의 모든 항목을 즉시 읽음 처리. - * patchUiPreferences immediate=true 로 디바운스 없이 즉시 DB에 반영해 - * 이후 폴링·STOMP 재수신 시에도 readIds 가 항상 최신 상태를 유지한다. + * 전체 닫기 — 현재 표시된 모든 알림을 읽음 처리하고 즉시 DB 저장. + * + * 핵심 원칙: + * 1. 사이드 이펙트(readIdsRef 갱신, DB 저장)는 setState 업데이터 밖에서 실행. + * 2. allNotificationsRef 로 현재 목록을 동기적으로 읽어 nextRead 를 구성. + * 3. immediate=true 로 400ms 디바운스 없이 즉시 저장해 이후 폴링과의 + * 경쟁 조건(readIds 가 아직 DB 에 없는 상태)을 제거. */ const nextRead = new Set(readIdsRef.current); toastNotificationsRef.current.forEach(n => nextRead.add(n.id)); - // allNotifications 는 state 업데이트 전이므로 ref가 아닌 최신 snapshot이 필요. - // setAllNotifications 콜백 안에서 nextRead를 채운 뒤 즉시 저장한다. - setAllNotifications(prev => { - prev.forEach(n => nextRead.add(n.id)); - readIdsRef.current = nextRead; - setReadIds(nextRead); - // immediate=true 로 400ms 디바운스 없이 즉시 저장 - patchUiPreferences({ tradeNotifications: { readIds: [...nextRead] } }, true); - return prev.map(n => ({ ...n, isRead: true })); - }); + allNotificationsRef.current.forEach(n => nextRead.add(n.id)); + + // 1) 인메모리 ref 즉시 갱신 — addNotification / refreshHistory 가 즉시 참조 + readIdsRef.current = nextRead; + + // 2) React state 갱신 (렌더링 예약) + setReadIds(nextRead); + setAllNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); setToastNotifications([]); + + // 3) 캐시 갱신 + 즉시 DB 저장 (immediate=true) + saveReadIds(nextRead, true); }, []); const markAllAsRead = useCallback(() => { - setAllNotifications(prev => { - const nextRead = new Set(loadReadIds()); - prev.forEach(n => nextRead.add(n.id)); - saveReadIds(nextRead); - readIdsRef.current = nextRead; - setReadIds(nextRead); - return prev.map(n => ({ ...n, isRead: true })); - }); + const nextRead = new Set(readIdsRef.current); + allNotificationsRef.current.forEach(n => nextRead.add(n.id)); + + readIdsRef.current = nextRead; + setReadIds(nextRead); + setAllNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); setToastNotifications([]); + saveReadIds(nextRead, true); }, []); const purgeNotifications = useCallback((ids: string[]) => {