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