알림오류 추가수정
This commit is contained in:
@@ -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[]) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user