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