알림삭제 기능 추가
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user