알림오류 추가수정

This commit is contained in:
Macbook
2026-06-04 16:00:03 +09:00
parent 938bb89323
commit cd711e44e7
@@ -266,23 +266,31 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const id = makeId(signal); const id = makeId(signal);
/** /**
* 이미 읽음(닫기) 처리된 알림 ID 이면: * 읽음 여부 3중 확인:
* - allNotifications 에 이미 존재하면 isRead 상태를 건드리지 않는다. * 1. readIdsRef.current — dismissAllToasts 가 즉시 업데이트하는 인메모리 ref
* - 새로운 항목이면 isRead: true 로 조용히 이력에만 추가한다. * 2. loadReadIds() — DB 캐시 기반 값
* - 토스트 팝업·알림음은 표시하지 않는다. * 3. allNotificationsRef — dismissAllToasts 가 functional updater 로 isRead:true
* STOMP 재연결 시 동일 신호가 재수신되거나 20 초 폴링에서 새로 발견되어도 * 를 예약했지만 readIdsRef 에 아직 반영되지 않은 경우를 커버
* 사용자가 명시적으로 닫은 알림은 다시 열리지 않는다. * (STOMP/WebSocket 재연결 직후 동일 신호 재수신 시 팝업 방지)
*/ */
// readIdsRef.current (인메모리) 와 DB 캐시(loadReadIds) 모두 확인. const alreadyRead =
// 앱 시작 초기 설정 로드 전 STOMP 신호 수신 시에도 읽음 상태를 올바르게 판단한다. readIdsRef.current.has(id) ||
const alreadyRead = readIdsRef.current.has(id) || loadReadIds().has(id); loadReadIds().has(id) ||
(allNotificationsRef.current.find(n => n.id === id)?.isRead === true);
// allNotifications 에는 isRead:true 이지만 readIds 에 빠진 경우 보정
if (alreadyRead && !readIdsRef.current.has(id)) {
const next = new Set(readIdsRef.current);
next.add(id);
readIdsRef.current = next;
saveReadIds(next); // 디바운스 저장 (즉시 저장 불필요)
}
setAllNotifications(prev => { setAllNotifications(prev => {
const existing = prev.find(n => n.id === id); const existing = prev.find(n => n.id === id);
if (existing) { if (existing) {
// 읽음 처리된 항목은 isRead 를 false 로 되돌리지 않는다 // 읽음 처리된 항목은 isRead 를 false 로 되돌리지 않는다
if (existing.isRead || alreadyRead) return prev; if (existing.isRead || alreadyRead) return prev;
// 미읽음 항목은 receivedAt 만 갱신
return prev.map(n => return prev.map(n =>
n.id === id ? { ...n, isRead: false, receivedAt: Date.now() } : n, n.id === id ? { ...n, isRead: false, receivedAt: Date.now() } : n,
); );
@@ -359,36 +367,45 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
* 전체 닫기 — 현재 표시된 모든 알림을 읽음 처리하고 즉시 DB 저장. * 전체 닫기 — 현재 표시된 모든 알림을 읽음 처리하고 즉시 DB 저장.
* *
* 핵심 원칙: * 핵심 원칙:
* 1. 사이드 이펙트(readIdsRef 갱신, DB 저장)는 setState 업데이터 밖에서 실행. * 1. setAllNotifications 의 functional updater 안에서 nextRead 를 구성한다.
* 2. allNotificationsRef 로 현재 목록을 동기적으로 읽어 nextRead 를 구성. * updater 의 prev 는 같은 배치 내 addNotification 의 setAllNotifications 까지
* 3. immediate=true 로 400ms 디바운스 없이 즉시 저장해 이후 폴링과의 * 포함한 최신 상태이므로, dismissAllToasts 직전에 도착한 STOMP 신호도
* readIds 에 포함된다 (stale ref 문제 해결).
* 2. immediate=true 로 400ms 디바운스 없이 즉시 저장해 이후 폴링과의
* 경쟁 조건(readIds 가 아직 DB 에 없는 상태)을 제거. * 경쟁 조건(readIds 가 아직 DB 에 없는 상태)을 제거.
*/ */
const nextRead = new Set(readIdsRef.current); const baseRead = new Set(readIdsRef.current);
toastNotificationsRef.current.forEach(n => nextRead.add(n.id)); toastNotificationsRef.current.forEach(n => baseRead.add(n.id));
allNotificationsRef.current.forEach(n => nextRead.add(n.id));
// 1) 인메모리 ref 즉시 갱신 — addNotification / refreshHistory 가 즉시 참조 setAllNotifications(prev => {
// prev = 현재 배치의 최신 allNotifications (addNotification 반영 포함)
const nextRead = new Set(baseRead);
prev.forEach(n => nextRead.add(n.id));
// 인메모리 ref 즉시 갱신 (addNotification 이 다음 틱에 확인할 때 참조)
readIdsRef.current = nextRead; readIdsRef.current = nextRead;
// 2) React state 갱신 (렌더링 예약) // 캐시 갱신 + 즉시 DB 저장
setReadIds(nextRead);
setAllNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
setToastNotifications([]);
// 3) 캐시 갱신 + 즉시 DB 저장 (immediate=true)
saveReadIds(nextRead, true); saveReadIds(nextRead, true);
return prev.map(n => ({ ...n, isRead: true }));
});
setReadIds(() => readIdsRef.current);
setToastNotifications([]);
}, []); }, []);
const markAllAsRead = useCallback(() => { const markAllAsRead = useCallback(() => {
const nextRead = new Set(readIdsRef.current); const baseRead = new Set(readIdsRef.current);
allNotificationsRef.current.forEach(n => nextRead.add(n.id)); setAllNotifications(prev => {
const nextRead = new Set(baseRead);
prev.forEach(n => nextRead.add(n.id));
readIdsRef.current = nextRead; readIdsRef.current = nextRead;
setReadIds(nextRead);
setAllNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
setToastNotifications([]);
saveReadIds(nextRead, true); saveReadIds(nextRead, true);
return prev.map(n => ({ ...n, isRead: true }));
});
setReadIds(() => readIdsRef.current);
setToastNotifications([]);
}, []); }, []);
const purgeNotifications = useCallback((ids: string[]) => { const purgeNotifications = useCallback((ids: string[]) => {