알림삭제 재재수정

This commit is contained in:
Macbook
2026-06-04 16:21:31 +09:00
parent cd711e44e7
commit be7a8fe8b4
@@ -151,6 +151,12 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const [allNotifications, setAllNotifications] = useState<TradeNotificationItem[]>([]); const [allNotifications, setAllNotifications] = useState<TradeNotificationItem[]>([]);
const allNotificationsRef = useRef(allNotifications); const allNotificationsRef = useRef(allNotifications);
allNotificationsRef.current = allNotifications; allNotificationsRef.current = allNotifications;
/**
* addNotification 에서 수신된 모든 알림 ID 를 동기적으로 누적.
* dismissAllToasts 가 호출될 때 allNotificationsRef 가 stale 한 경우에도
* 같은 틱에 수신된 신호 ID 까지 readIds 에 포함시킬 수 있다.
*/
const allSeenIdsRef = useRef<Set<string>>(new Set());
const [detailSignal, setDetailSignal] = useState<TradeSignalInfo | null>(null); const [detailSignal, setDetailSignal] = useState<TradeSignalInfo | null>(null);
const [detailCentered, setDetailCentered] = useState(false); const [detailCentered, setDetailCentered] = useState(false);
@@ -237,10 +243,12 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
* 앱 설정이 DB 에서 로드 완료된 시점에 readIds 를 즉시 동기화한다. * 앱 설정이 DB 에서 로드 완료된 시점에 readIds 를 즉시 동기화한다.
* STOMP 가 앱 시작 직후 구독 시 readIds 가 아직 빈 상태여서 이미 닫은 * STOMP 가 앱 시작 직후 구독 시 readIds 가 아직 빈 상태여서 이미 닫은
* 알림이 다시 팝업으로 뜨는 타이밍 버그를 방지한다. * 알림이 다시 팝업으로 뜨는 타이밍 버그를 방지한다.
* 세션 전환 시 allSeenIdsRef 도 초기화해 이전 세션 알림 ID 가 잔류하지 않게 한다.
*/ */
allSeenIdsRef.current = new Set();
const dbRead = loadReadIds(); const dbRead = loadReadIds();
if (dbRead.size > 0) { const merged = new Set([...readIdsRef.current, ...dbRead]);
const merged = new Set([...readIdsRef.current, ...dbRead]); if (merged.size !== readIdsRef.current.size) {
readIdsRef.current = merged; readIdsRef.current = merged;
setReadIds(merged); setReadIds(merged);
} }
@@ -265,25 +273,27 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const addNotification = useCallback((signal: TradeSignalInfo & { dbId?: number }) => { const addNotification = useCallback((signal: TradeSignalInfo & { dbId?: number }) => {
const id = makeId(signal); const id = makeId(signal);
// 수신한 모든 ID 를 동기적으로 누적 (dismissAllToasts 가 참조)
allSeenIdsRef.current.add(id);
/** /**
* 읽음 여부 3중 확인: * 읽음 여부 3중 확인:
* 1. readIdsRef.current — dismissAllToasts 가 즉시 업데이트하는 인메모리 ref * 1. readIdsRef.current — dismissAllToasts 가 즉시 업데이트하는 인메모리 ref
* 2. loadReadIds() — DB 캐시 기반 값 * 2. loadReadIds() — DB 캐시 기반 값
* 3. allNotificationsRef — dismissAllToasts functional updater 로 isRead:true * 3. allNotificationsRef — dismissAllToasts functional updater
* 를 예약했지만 readIdsRef 에 아직 반영되지 않은 경우 커버 * isRead:true 를 예약했지만 readIdsRef 에 아직 반영되지 않은 경우 커버
* (STOMP/WebSocket 재연결 직후 동일 신호 재수신 시 팝업 방지)
*/ */
const alreadyRead = const alreadyRead =
readIdsRef.current.has(id) || readIdsRef.current.has(id) ||
loadReadIds().has(id) || loadReadIds().has(id) ||
(allNotificationsRef.current.find(n => n.id === id)?.isRead === true); (allNotificationsRef.current.find(n => n.id === id)?.isRead === true);
// allNotifications 에는 isRead:true 이지만 readIds 에 빠진 경우 보정 // readIds 에 누락된 읽음 항목 보정 (다음 STOMP/폴링에서 즉시 필터링)
if (alreadyRead && !readIdsRef.current.has(id)) { if (alreadyRead && !readIdsRef.current.has(id)) {
const next = new Set(readIdsRef.current); const next = new Set(readIdsRef.current);
next.add(id); next.add(id);
readIdsRef.current = next; readIdsRef.current = next;
saveReadIds(next); // 디바운스 저장 (즉시 저장 불필요) saveReadIds(next);
} }
setAllNotifications(prev => { setAllNotifications(prev => {
@@ -364,48 +374,44 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const dismissAllToasts = useCallback(() => { const dismissAllToasts = useCallback(() => {
/** /**
* 전체 닫기 — 현재 표시된 모든 알림을 읽음 처리하고 즉시 DB 저장. * 전체 닫기 — 모든 알림을 읽음 처리하고 즉시 DB 저장.
* *
* 핵심 원칙: * allSeenIdsRef: addNotification 에서 동기적으로 누적된 ID 집합.
* 1. setAllNotifications 의 functional updater 안에서 nextRead 를 구성한다. * - allNotificationsRef.current 가 stale 하더라도(배치 미반영)
* updater 의 prev 는 같은 배치 내 addNotification 의 setAllNotifications 까지 * 동일 틱에 수신된 STOMP 신호 ID 도 포함 → readIds 에서 누락 없음.
* 포함한 최신 상태이므로, dismissAllToasts 직전에 도착한 STOMP 신호도 * side-effect(readIdsRef, saveReadIds)를 functional updater 밖에서 실행하여
* readIds 에 포함된다 (stale ref 문제 해결). * React concurrent 렌더링에서의 중복 실행 문제를 방지.
* 2. immediate=true 로 400ms 디바운스 없이 즉시 저장해 이후 폴링과의
* 경쟁 조건(readIds 가 아직 DB 에 없는 상태)을 제거.
*/ */
const baseRead = new Set(readIdsRef.current); const nextRead = new Set([
toastNotificationsRef.current.forEach(n => baseRead.add(n.id)); ...readIdsRef.current,
...toastNotificationsRef.current.map(n => n.id),
...allNotificationsRef.current.map(n => n.id),
...allSeenIdsRef.current, // 동일 틱에 수신됐지만 ref 미반영 ID
]);
setAllNotifications(prev => { // 1) 인메모리 ref 즉시 갱신 (다음 addNotification 호출에서 즉시 참조)
// prev = 현재 배치의 최신 allNotifications (addNotification 반영 포함) readIdsRef.current = nextRead;
const nextRead = new Set(baseRead);
prev.forEach(n => nextRead.add(n.id));
// 인메모리 ref 즉시 갱신 (addNotification 이 다음 틱에 확인할 때 참조) // 2) React state 갱신
readIdsRef.current = nextRead; setReadIds(nextRead);
setAllNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
// 캐시 갱신 + 즉시 DB 저장
saveReadIds(nextRead, true);
return prev.map(n => ({ ...n, isRead: true }));
});
setReadIds(() => readIdsRef.current);
setToastNotifications([]); setToastNotifications([]);
// 3) 인메모리 캐시 + 즉시 DB 저장 (immediate=true)
saveReadIds(nextRead, true);
}, []); }, []);
const markAllAsRead = useCallback(() => { const markAllAsRead = useCallback(() => {
const baseRead = new Set(readIdsRef.current); const nextRead = new Set([
setAllNotifications(prev => { ...readIdsRef.current,
const nextRead = new Set(baseRead); ...allNotificationsRef.current.map(n => n.id),
prev.forEach(n => nextRead.add(n.id)); ...allSeenIdsRef.current,
readIdsRef.current = nextRead; ]);
saveReadIds(nextRead, true); readIdsRef.current = nextRead;
return prev.map(n => ({ ...n, isRead: true })); setReadIds(nextRead);
}); setAllNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
setReadIds(() => readIdsRef.current);
setToastNotifications([]); setToastNotifications([]);
saveReadIds(nextRead, true);
}, []); }, []);
const purgeNotifications = useCallback((ids: string[]) => { const purgeNotifications = useCallback((ids: string[]) => {