매매 시그널 알림
-
- {isListView
- ? '각 알림 왼쪽은 가상매매와 동일한 신호 상세 카드, 오른쪽은 캔들·지표 차트입니다. 영역 우측 버튼으로 가로 스크롤할 수 있습니다.'
- : `목록형과 동일한 알림 상세 카드를 ${getTradeNotificationGridPreset(gridPreset).label} 그리드로 표시합니다. 툴바 우측에서 배치를 변경할 수 있습니다.`}
-
미확인 {unreadCount}건
+ {selectedCount > 0 && (
+
선택 {selectedCount}건
+ )}
= ({
미확인
-
void refreshHistory()}>
- 새로고침
-
{unreadCount > 0 && (
모두 읽음
@@ -262,6 +296,16 @@ export const TradeNotificationListPage: React.FC = ({
그리드형
+
void handlePageRefresh()}
+ >
+
+
= ({
void handleDeleteUnread()}
+ title={selectedCount > 0 ? `선택 알림 삭제 (${selectedCount}건)` : '선택 알림 삭제'}
+ aria-label={selectedCount > 0 ? `선택 알림 ${selectedCount}건 삭제` : '선택 알림 삭제'}
+ disabled={selectedCount === 0}
+ onClick={() => void handleDeleteSelected()}
>
@@ -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="알림 요약"
>
-
+
{primary}
@@ -264,7 +270,7 @@ const TradeNotificationListRow: React.FC
= ({
{strategyLabel}
-
+
= ({
isSelected ? 'tnl-row--selected' : '',
receiving ? 'tnl-row--receiving' : '',
].filter(Boolean).join(' ')}
+ onClick={handleRowClick}
>
{summaryCard}
@@ -313,6 +320,7 @@ const TradeNotificationListRow: React.FC = ({
isSelected ? 'tnl-row--selected' : '',
receiving ? 'tnl-row--receiving' : '',
].filter(Boolean).join(' ')}
+ onClick={handleRowClick}
>
void;
/** 알림 목록에서 단건 삭제 */
deleteNotification: (id: string) => Promise
;
+ /** 지정 id 알림 일괄 삭제 (목록 선택 삭제) */
+ deleteNotifications: (ids: string[]) => Promise;
/** 읽지 않은(미확인) 알림만 삭제 */
deleteUnreadNotifications: () => Promise;
/** 알림 목록 전체 삭제 */
@@ -377,33 +379,33 @@ export const TradeNotificationProvider: React.FC = ({
?? 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);
- }
+ const deleteNotifications = useCallback(async (ids: string[]) => {
+ const unique = [...new Set(ids)].filter(Boolean);
+ if (unique.length === 0) return;
+ const dbIds: number[] = [];
+ for (const id of unique) {
+ const item = findNotificationById(id);
+ if (item?.dbId != null) dbIds.push(item.dbId);
}
- }, [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);
+ purgeNotifications(unique);
if (dbIds.length > 0) {
try {
await deleteTradeSignalsBatch(dbIds);
} catch (e) {
- console.warn('[TradeNotification] 미확인 일괄 삭제 실패:', e);
+ console.warn('[TradeNotification] 일괄 삭제 실패:', e);
}
}
- }, [allNotifications, purgeNotifications]);
+ }, [findNotificationById, purgeNotifications]);
+
+ const deleteNotification = useCallback(async (id: string) => {
+ await deleteNotifications([id]);
+ }, [deleteNotifications]);
+
+ const deleteUnreadNotifications = useCallback(async () => {
+ const unread = allNotifications.filter(n => !n.isRead);
+ if (unread.length === 0) return;
+ await deleteNotifications(unread.map(n => n.id));
+ }, [allNotifications, deleteNotifications]);
const deleteAllNotifications = useCallback(async () => {
const ids = [
@@ -454,6 +456,7 @@ export const TradeNotificationProvider: React.FC = ({
markAsRead,
markAllAsRead,
deleteNotification,
+ deleteNotifications,
deleteUnreadNotifications,
deleteAllNotifications,
refreshHistory,
diff --git a/frontend/src/styles/tradeNotificationList.css b/frontend/src/styles/tradeNotificationList.css
index dd529da..c4d0d19 100644
--- a/frontend/src/styles/tradeNotificationList.css
+++ b/frontend/src/styles/tradeNotificationList.css
@@ -1033,12 +1033,25 @@ ul.tnl-list.tnl-list--grid {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #7aa2f7) 35%, transparent);
}
+.tnl-row--gallery.tnl-row--selected,
+.tnl-row--grid.tnl-row--selected {
+ border-color: var(--accent, #7aa2f7) !important;
+ box-shadow:
+ 0 0 0 2px color-mix(in srgb, var(--accent, #7aa2f7) 45%, transparent),
+ 0 0 12px 2px color-mix(in srgb, var(--accent, #7aa2f7) 18%, transparent);
+}
+
.tnl-row--gallery.tnl-row--selected .tnl-hscroll-pane,
.tnl-row--gallery.tnl-row--selected .tnl-summary-card--selected {
border-color: var(--accent, #7aa2f7);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #7aa2f7) 35%, transparent);
}
+.tnl-row--gallery,
+.tnl-row--grid {
+ cursor: pointer;
+}
+
.tnl-list--gallery .tnl-row--gallery {
width: 100%;
}