알림삭제 기능 추가

This commit is contained in:
Macbook
2026-05-27 17:39:33 +09:00
parent 701a9b2881
commit 9cee6387c3
7 changed files with 344 additions and 4 deletions
@@ -14,7 +14,13 @@ import React, {
useState,
} from 'react';
import type { TradeSignalInfo } from '../components/TradeAlertModal';
import { loadTradeSignals, type TradeSignalDto } from '../utils/backendApi';
import {
deleteAllTradeSignals,
deleteTradeSignal,
deleteTradeSignalsBatch,
loadTradeSignals,
type TradeSignalDto,
} from '../utils/backendApi';
import {
normalizeTradeAlertSoundId,
playTradeAlertSound,
@@ -22,6 +28,7 @@ import {
} from '../utils/tradeAlertSound';
const STORAGE_KEY = 'gc_trade_notify_read_v1';
const HIDDEN_STORAGE_KEY = 'gc_trade_notify_hidden_v1';
const MAX_HISTORY = 300;
/** 팝업 대기열 최대 건수 (페이지 슬라이딩) */
const MAX_TOAST_QUEUE = 50;
@@ -48,6 +55,12 @@ interface TradeNotificationContextValue {
dismissAllToasts: () => void;
markAsRead: (id: string) => void;
markAllAsRead: () => void;
/** 알림 목록에서 단건 삭제 */
deleteNotification: (id: string) => Promise<void>;
/** 읽지 않은(미확인) 알림만 삭제 */
deleteUnreadNotifications: () => Promise<void>;
/** 알림 목록 전체 삭제 */
deleteAllNotifications: () => Promise<void>;
refreshHistory: () => Promise<void>;
detailSignal: TradeSignalInfo | null;
/** 상세 모달을 화면 중앙에 표시 (알림 목록에서 열 때) */
@@ -96,6 +109,22 @@ function saveReadIds(ids: Set<string>) {
} catch { /* ignore */ }
}
function loadHiddenIds(): Set<string> {
try {
const raw = localStorage.getItem(HIDDEN_STORAGE_KEY);
if (!raw) return new Set();
return new Set(JSON.parse(raw) as string[]);
} catch {
return new Set();
}
}
function saveHiddenIds(ids: Set<string>) {
try {
localStorage.setItem(HIDDEN_STORAGE_KEY, JSON.stringify([...ids]));
} catch { /* ignore */ }
}
export function useTradeNotification(): TradeNotificationContextValue {
const ctx = useContext(TradeNotificationContext);
if (!ctx) {
@@ -141,7 +170,10 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
const dtos = await loadTradeSignals();
/** dismiss 직후 비동기 완료 시 state readIds가 늦게 반영되는 것 방지 */
const read = loadReadIds();
const items = dtos.map(d => dtoToItem(d, read));
const hidden = loadHiddenIds();
const items = dtos
.map(d => dtoToItem(d, read))
.filter(item => !hidden.has(item.id));
let newlyArrived: TradeNotificationItem[] = [];
@@ -156,10 +188,12 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
map.set(n.id, wasRead ? { ...n, isRead: true } : n);
}
for (const n of prev) {
if (hidden.has(n.id)) continue;
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()]
.filter(n => !hidden.has(n.id))
.sort((a, b) => b.receivedAt - a.receivedAt)
.slice(0, MAX_HISTORY);
});
@@ -312,6 +346,86 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
setToastNotifications([]);
}, []);
const purgeNotifications = useCallback((ids: string[]) => {
if (ids.length === 0) return;
const idSet = new Set(ids);
const hidden = loadHiddenIds();
ids.forEach(id => hidden.add(id));
saveHiddenIds(hidden);
setReadIds(prev => {
const next = new Set(prev);
ids.forEach(id => next.delete(id));
saveReadIds(next);
readIdsRef.current = next;
return next;
});
setToastNotifications(prev => prev.filter(n => !idSet.has(n.id)));
setAllNotifications(prev => prev.filter(n => !idSet.has(n.id)));
if (detailSignal) {
const detailId = makeId({
market: detailSignal.market,
candleTime: detailSignal.candleTime,
signalType: detailSignal.signalType,
});
if (idSet.has(detailId)) {
setDetailSignal(null);
setDetailCentered(false);
}
}
}, [detailSignal]);
const findNotificationById = useCallback((id: string): TradeNotificationItem | undefined => {
return allNotifications.find(n => n.id === id)
?? toastNotificationsRef.current.find(n => n.id === id);
}, [allNotifications]);
const deleteNotification = useCallback(async (id: string) => {
const item = findNotificationById(id);
purgeNotifications([id]);
if (item?.dbId != null) {
try {
await deleteTradeSignal(item.dbId);
} catch (e) {
console.warn('[TradeNotification] 서버 삭제 실패:', e);
}
}
}, [findNotificationById, purgeNotifications]);
const deleteUnreadNotifications = useCallback(async () => {
const snapshot = [...allNotifications];
const unread = snapshot.filter(n => !n.isRead);
if (unread.length === 0) return;
const ids = unread.map(n => n.id);
const dbIds = unread.map(n => n.dbId).filter((x): x is number => x != null);
purgeNotifications(ids);
if (dbIds.length > 0) {
try {
await deleteTradeSignalsBatch(dbIds);
} catch (e) {
console.warn('[TradeNotification] 미확인 일괄 삭제 실패:', e);
}
}
}, [allNotifications, purgeNotifications]);
const deleteAllNotifications = useCallback(async () => {
const ids = [
...new Set([
...allNotifications.map(n => n.id),
...toastNotificationsRef.current.map(n => n.id),
]),
];
purgeNotifications(ids);
setToastNotifications([]);
saveReadIds(new Set());
saveHiddenIds(new Set());
try {
await deleteAllTradeSignals();
} catch (e) {
console.warn('[TradeNotification] 전체 삭제 실패:', e);
}
}, [allNotifications, purgeNotifications]);
const openDetail = useCallback((item: TradeNotificationItem, options?: { centered?: boolean }) => {
setDetailCentered(options?.centered ?? false);
setDetailSignal({
@@ -342,6 +456,9 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
dismissAllToasts,
markAsRead,
markAllAsRead,
deleteNotification,
deleteUnreadNotifications,
deleteAllNotifications,
refreshHistory,
detailSignal,
detailCentered,