From 19ae5f5faeba8dabe39f5a19aa565a26cd9ad79e Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 29 May 2026 01:05:52 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=A4=EB=A7=A4=20=EC=8B=9C=EA=B7=B8?= =?UTF-8?q?=EB=84=90=EC=95=8C=EB=A6=BC=20=ED=99=94=EB=A9=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 4 + .../components/TradeNotificationListPage.tsx | 108 ++++++++++++------ .../TradeNotificationListRow.tsx | 20 +++- .../src/contexts/TradeNotificationContext.tsx | 43 +++---- frontend/src/styles/tradeNotificationList.css | 13 +++ 5 files changed, 130 insertions(+), 58 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index fc3f85b..4c01037 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -11549,6 +11549,9 @@ html.theme-light .tam-disclaimer { color: #90a4ae; } color: #f7768e; border-color: rgba(247, 118, 142, 0.55); } +.tnl-icon-btn--refreshing svg { + animation: spin 0.8s linear infinite; +} .tnl-row-icon-btn { flex-shrink: 0; align-self: center; @@ -11576,6 +11579,7 @@ html.theme-light .tam-disclaimer { color: #90a4ae; } background: var(--bg3); } .tnl-chip--warn { background: rgba(239,83,80,0.15); color: #f7768e; } +.tnl-chip--selected { background: rgba(122,162,247,0.18); color: var(--accent, #7aa2f7); font-weight: 600; } .tnl-filter { display: flex; gap: 4px; } .tnl-filter button, .tnl-btn { diff --git a/frontend/src/components/TradeNotificationListPage.tsx b/frontend/src/components/TradeNotificationListPage.tsx index 2d2df4c..2699ea8 100644 --- a/frontend/src/components/TradeNotificationListPage.tsx +++ b/frontend/src/components/TradeNotificationListPage.tsx @@ -41,6 +41,14 @@ const IcTrash = () => ( ); +const IcRefresh = () => ( + + + + + +); + interface Props { theme: Theme; onGoToChart: (market: string) => void; @@ -77,7 +85,7 @@ export const TradeNotificationListPage: React.FC = ({ markAsRead, markAllAsRead, deleteNotification, - deleteUnreadNotifications, + deleteNotifications, deleteAllNotifications, openDetail, refreshHistory, @@ -91,12 +99,13 @@ export const TradeNotificationListPage: React.FC = ({ () => loadTradeNotificationGridPreset(), ); const [selectedMarket, setSelectedMarket] = useState(defaultMarket); - const [selectedNotifyId, setSelectedNotifyId] = useState(null); + const [selectedIds, setSelectedIds] = useState>(() => new Set()); const [rightTab, setRightTab] = useState('trade'); const [fillBuy, setFillBuy] = useState(null); const [fillSell, setFillSell] = useState(null); const [summary, setSummary] = useState(null); const orderAnchorRef = useRef(null); + const [refreshing, setRefreshing] = useState(false); const { settings: appSettings } = useAppSettings(); const chartLiveReceiveHighlight = appSettings.chartLiveReceiveHighlight ?? true; @@ -150,37 +159,54 @@ export const TradeNotificationListPage: React.FC = ({ setRightTab('trade'); }, [tickers]); + const selectedCount = selectedIds.size; + const handleNotificationSelect = useCallback((item: TradeNotificationItem) => { if (!item.isRead) markAsRead(item.id); - setSelectedNotifyId(item.id); - const price = coerceFiniteNumber(item.price); - if (price != null && price > 0) { - fillBothSides(item.market, price, setFillBuy, setFillSell); - } else { - handleSelectMarket(item.market); - } - setRightTab('trade'); + setSelectedIds(prev => { + const adding = !prev.has(item.id); + const next = new Set(prev); + if (adding) { + next.add(item.id); + const price = coerceFiniteNumber(item.price); + if (price != null && price > 0) { + fillBothSides(item.market, price, setFillBuy, setFillSell); + } else { + handleSelectMarket(item.market); + } + setRightTab('trade'); + } else { + next.delete(item.id); + } + return next; + }); }, [markAsRead, handleSelectMarket]); const handleDeleteOne = useCallback(async (item: TradeNotificationItem, e: React.MouseEvent) => { e.stopPropagation(); if (!window.confirm('이 알림을 삭제하시겠습니까?')) return; await deleteNotification(item.id); - if (selectedNotifyId === item.id) setSelectedNotifyId(null); - }, [deleteNotification, selectedNotifyId]); + setSelectedIds(prev => { + if (!prev.has(item.id)) return prev; + const next = new Set(prev); + next.delete(item.id); + return next; + }); + }, [deleteNotification]); - const handleDeleteUnread = useCallback(async () => { - if (unreadCount === 0) return; - if (!window.confirm(`읽지 않은 알림 ${unreadCount}건을 삭제하시겠습니까?`)) return; - await deleteUnreadNotifications(); - setSelectedNotifyId(null); - }, [unreadCount, deleteUnreadNotifications]); + const handleDeleteSelected = useCallback(async () => { + if (selectedCount === 0) return; + const ids = [...selectedIds]; + if (!window.confirm(`선택한 알림 ${ids.length}건을 삭제하시겠습니까?`)) return; + await deleteNotifications(ids); + setSelectedIds(new Set()); + }, [selectedCount, selectedIds, deleteNotifications]); const handleDeleteAll = useCallback(async () => { if (allNotifications.length === 0) return; if (!window.confirm(`알림 ${allNotifications.length}건을 모두 삭제하시겠습니까?`)) return; await deleteAllNotifications(); - setSelectedNotifyId(null); + setSelectedIds(new Set()); }, [allNotifications.length, deleteAllNotifications]); const handleOrderbookRowClick = useCallback((price: number, rowType: 'ask' | 'bid') => { @@ -200,6 +226,19 @@ export const TradeNotificationListPage: React.FC = ({ saveTradeNotificationListLayout(layout); }, []); + const handlePageRefresh = useCallback(async () => { + if (refreshing) return; + setRefreshing(true); + try { + await Promise.all([ + refreshHistory({ serverOnly: true }), + refreshPaperData(), + ]); + } finally { + setRefreshing(false); + } + }, [refreshing, refreshHistory, refreshPaperData]); + const handleGridPresetChange = useCallback((preset: TradeNotificationGridPresetId) => { setGridPreset(preset); saveTradeNotificationGridPreset(preset); @@ -214,13 +253,11 @@ export const TradeNotificationListPage: React.FC = ({

매매 시그널 알림

-

- {isListView - ? '각 알림 왼쪽은 가상매매와 동일한 신호 상세 카드, 오른쪽은 캔들·지표 차트입니다. 영역 우측 버튼으로 가로 스크롤할 수 있습니다.' - : `목록형과 동일한 알림 상세 카드를 ${getTradeNotificationGridPreset(gridPreset).label} 그리드로 표시합니다. 툴바 우측에서 배치를 변경할 수 있습니다.`} -

미확인 {unreadCount}건 + {selectedCount > 0 && ( + 선택 {selectedCount}건 + )}
- {unreadCount > 0 && (
+ = ({ @@ -310,7 +354,7 @@ export const TradeNotificationListPage: React.FC = ({ item={item} theme={theme} layoutMode={listLayout} - isSelected={selectedNotifyId === item.id} + isSelected={selectedIds.has(item.id)} chartsEnabled={isListView} chartLiveReceiveHighlight={chartLiveReceiveHighlight} onSelect={() => handleNotificationSelect(item)} diff --git a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx index 8136987..d5c6a1d 100644 --- a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx +++ b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx @@ -197,6 +197,16 @@ const TradeNotificationListRow: React.FC = ({ [chartCardCount], ); + const handleRowClick = useCallback((e: React.MouseEvent) => { + const t = e.target as HTMLElement; + if (t.closest( + 'button, a, input, select, textarea, .tnl-chart-view-btn, .tnl-hscroll-pane__rail-btn', + )) { + return; + } + onSelect(); + }, [onSelect]); + const summaryCard = (
= ({ ].filter(Boolean).join(' ')} aria-label="알림 요약" > - +