매매 시그널알림 화면 수정

This commit is contained in:
Macbook
2026-05-29 01:05:52 +09:00
parent e1ad1d626e
commit 19ae5f5fae
5 changed files with 130 additions and 58 deletions
@@ -41,6 +41,14 @@ const IcTrash = () => (
</svg>
);
const IcRefresh = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<polyline points="23 4 23 10 17 10" />
<polyline points="1 20 1 14 7 14" />
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
</svg>
);
interface Props {
theme: Theme;
onGoToChart: (market: string) => void;
@@ -77,7 +85,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
markAsRead,
markAllAsRead,
deleteNotification,
deleteUnreadNotifications,
deleteNotifications,
deleteAllNotifications,
openDetail,
refreshHistory,
@@ -91,12 +99,13 @@ export const TradeNotificationListPage: React.FC<Props> = ({
() => loadTradeNotificationGridPreset(),
);
const [selectedMarket, setSelectedMarket] = useState(defaultMarket);
const [selectedNotifyId, setSelectedNotifyId] = useState<string | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
const [rightTab, setRightTab] = useState<RightTab>('trade');
const [fillBuy, setFillBuy] = useState<TradeOrderFillRequest | null>(null);
const [fillSell, setFillSell] = useState<TradeOrderFillRequest | null>(null);
const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
const orderAnchorRef = useRef<HTMLDivElement>(null);
const [refreshing, setRefreshing] = useState(false);
const { settings: appSettings } = useAppSettings();
const chartLiveReceiveHighlight = appSettings.chartLiveReceiveHighlight ?? true;
@@ -150,37 +159,54 @@ export const TradeNotificationListPage: React.FC<Props> = ({
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<Props> = ({
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<Props> = ({
<div className="tnl-main" style={{ containerType: 'inline-size', containerName: 'tnl-main' }}>
<div className="tnl-header">
<h1 className="tnl-title"> </h1>
<p className="tnl-sub">
{isListView
? '각 알림 왼쪽은 가상매매와 동일한 신호 상세 카드, 오른쪽은 캔들·지표 차트입니다. 영역 우측 버튼으로 가로 스크롤할 수 있습니다.'
: `목록형과 동일한 알림 상세 카드를 ${getTradeNotificationGridPreset(gridPreset).label} 그리드로 표시합니다. 툴바 우측에서 배치를 변경할 수 있습니다.`}
</p>
<div className="tnl-toolbar">
<span className="tnl-chip tnl-chip--warn"> {unreadCount}</span>
{selectedCount > 0 && (
<span className="tnl-chip tnl-chip--selected"> {selectedCount}</span>
)}
<div className="tnl-filter">
<button
type="button"
@@ -237,9 +274,6 @@ export const TradeNotificationListPage: React.FC<Props> = ({
</button>
</div>
<button type="button" className="tnl-btn" onClick={() => void refreshHistory()}>
</button>
{unreadCount > 0 && (
<button type="button" className="tnl-btn tnl-btn--primary" onClick={markAllAsRead}>
@@ -262,6 +296,16 @@ export const TradeNotificationListPage: React.FC<Props> = ({
</button>
</div>
<button
type="button"
className={['tnl-icon-btn', refreshing ? 'tnl-icon-btn--refreshing' : ''].filter(Boolean).join(' ')}
title="화면 새로고침"
aria-label="화면 새로고침"
disabled={refreshing}
onClick={() => void handlePageRefresh()}
>
<IcRefresh />
</button>
<TradeNotificationGridLayoutPicker
value={gridPreset}
onChange={handleGridPresetChange}
@@ -271,10 +315,10 @@ export const TradeNotificationListPage: React.FC<Props> = ({
<button
type="button"
className="tnl-icon-btn"
title="미확인 알림 삭제"
aria-label="미확인 알림 삭제"
disabled={unreadCount === 0}
onClick={() => void handleDeleteUnread()}
title={selectedCount > 0 ? `선택 알림 삭제 (${selectedCount}건)` : '선택 알림 삭제'}
aria-label={selectedCount > 0 ? `선택 알림 ${selectedCount}건 삭제` : '선택 알림 삭제'}
disabled={selectedCount === 0}
onClick={() => void handleDeleteSelected()}
>
<IcTrash />
</button>
@@ -310,7 +354,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
item={item}
theme={theme}
layoutMode={listLayout}
isSelected={selectedNotifyId === item.id}
isSelected={selectedIds.has(item.id)}
chartsEnabled={isListView}
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
onSelect={() => handleNotificationSelect(item)}