전략알림 수정

This commit is contained in:
Macbook
2026-05-27 16:51:57 +09:00
parent d7ceb8cbfd
commit aca895e9fd
13 changed files with 701 additions and 123 deletions
@@ -122,6 +122,8 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
}) => {
const alertSoundId = normalizeTradeAlertSoundId(soundId) as TradeAlertSoundId;
const [readIds, setReadIds] = useState<Set<string>>(() => loadReadIds());
const readIdsRef = useRef(readIds);
readIdsRef.current = readIds;
const [toastNotifications, setToastNotifications] = useState<TradeNotificationItem[]>([]);
const toastNotificationsRef = useRef(toastNotifications);
toastNotificationsRef.current = toastNotifications;
@@ -137,38 +139,45 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const refreshHistory = useCallback(async () => {
try {
const dtos = await loadTradeSignals();
const items = dtos.map(d => dtoToItem(d, readIds));
/** dismiss 직후 비동기 완료 시 state readIds가 늦게 반영되는 것 방지 */
const read = loadReadIds();
const items = dtos.map(d => dtoToItem(d, read));
let newlyArrived: TradeNotificationItem[] = [];
setAllNotifications(prev => {
const isBoot = prev.length === 0;
const newlyArrived = items.filter(
newlyArrived = items.filter(
item => !item.isRead && !prev.some(p => p.id === item.id),
);
if (!isBoot && popupEnabled && newlyArrived.length > 0) {
setToastNotifications(tprev => {
const map = new Map(tprev.map(t => [t.id, t]));
for (const n of newlyArrived) map.set(n.id, n);
return [...map.values()].slice(0, MAX_TOAST_QUEUE);
});
if (soundEnabled) {
playTradeAlertSound(alertSoundId);
}
}
const map = new Map<string, TradeNotificationItem>();
for (const n of items) map.set(n.id, n);
for (const n of items) {
const wasRead = read.has(n.id) || prev.find(p => p.id === n.id)?.isRead;
map.set(n.id, wasRead ? { ...n, isRead: true } : n);
}
for (const n of prev) {
if (!map.has(n.id)) map.set(n.id, n);
else if (n.isRead) map.set(n.id, { ...map.get(n.id)!, isRead: true });
}
return [...map.values()]
.sort((a, b) => b.receivedAt - a.receivedAt)
.slice(0, MAX_HISTORY);
});
if (popupEnabled && newlyArrived.length > 0) {
setToastNotifications(tprev => {
const map = new Map(tprev.map(t => [t.id, t]));
for (const n of newlyArrived) map.set(n.id, n);
return [...map.values()].slice(0, MAX_TOAST_QUEUE);
});
if (soundEnabled) {
playTradeAlertSound(alertSoundId);
}
}
} catch (e) {
console.warn('[TradeNotification] 이력 로드 실패:', e);
}
}, [readIds, popupEnabled, soundEnabled, alertSoundId]);
}, [popupEnabled, soundEnabled, alertSoundId]);
useEffect(() => {
void refreshHistory();
@@ -179,6 +188,7 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const next = new Set(prev);
next.add(id);
saveReadIds(next);
readIdsRef.current = next;
return next;
});
setToastNotifications(prev => prev.filter(n => n.id !== id));
@@ -195,6 +205,7 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const next = new Set(prev);
next.delete(id);
saveReadIds(next);
readIdsRef.current = next;
return next;
});
const item: TradeNotificationItem = {
@@ -238,16 +249,30 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const dismissToasts = useCallback((ids: string[]) => {
if (ids.length === 0) return;
const idSet = new Set(ids);
const toastSnapshot = toastNotificationsRef.current;
setReadIds(r => {
const next = new Set(r);
ids.forEach(i => next.add(i));
saveReadIds(next);
readIdsRef.current = next;
return next;
});
setToastNotifications(prev => prev.filter(n => !idSet.has(n.id)));
setAllNotifications(all =>
all.map(n => (idSet.has(n.id) ? { ...n, isRead: true } : n)),
);
setAllNotifications(all => {
const byId = new Map(all.map(n => [n.id, n]));
for (const id of ids) {
const existing = byId.get(id);
if (existing) {
byId.set(id, { ...existing, isRead: true });
} else {
const fromToast = toastSnapshot.find(n => n.id === id);
if (fromToast) byId.set(id, { ...fromToast, isRead: true });
}
}
return [...byId.values()]
.sort((a, b) => b.receivedAt - a.receivedAt)
.slice(0, MAX_HISTORY);
});
}, []);
const dismissAllToasts = useCallback(() => {
@@ -259,24 +284,33 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const next = new Set(r);
idSet.forEach(i => next.add(i));
saveReadIds(next);
readIdsRef.current = next;
return next;
});
setToastNotifications([]);
setAllNotifications(all =>
all.map(n => (idSet.has(n.id) ? { ...n, isRead: true } : n)),
);
setAllNotifications(all => {
const byId = new Map(all.map(n => [n.id, n]));
for (const n of snapshot) {
const existing = byId.get(n.id);
byId.set(n.id, existing ? { ...existing, isRead: true } : { ...n, isRead: true });
}
return [...byId.values()]
.sort((a, b) => b.receivedAt - a.receivedAt)
.slice(0, MAX_HISTORY);
});
}, []);
const markAllAsRead = useCallback(() => {
setAllNotifications(prev => {
const nextRead = new Set(readIds);
const nextRead = new Set(loadReadIds());
prev.forEach(n => nextRead.add(n.id));
saveReadIds(nextRead);
readIdsRef.current = nextRead;
setReadIds(nextRead);
return prev.map(n => ({ ...n, isRead: true }));
});
setToastNotifications([]);
}, [readIds]);
}, []);
const openDetail = useCallback((item: TradeNotificationItem, options?: { centered?: boolean }) => {
setDetailCentered(options?.centered ?? false);