매매 시그널알림 화면 수정
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user