From c228f18c25f61bbec7eb78428c0343240c721d84 Mon Sep 17 00:00:00 2001 From: Macbook Date: Wed, 3 Jun 2026 11:33:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=8C=EB=A6=BC=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A2=85=EB=AA=A9=EB=B3=84=20=EC=A0=95=EB=A0=AC=EC=8B=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=B3=84=EB=A1=9C=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/virtual/VirtualHistoryScreen.tsx | 11 +++- .../components/TradeNotificationListPage.tsx | 21 +------ .../backtest/BacktestExecutionList.tsx | 17 +++++- .../paper/PaperTradeHistoryList.tsx | 13 +++-- frontend/src/utils/tradeListSort.ts | 57 +++++++++++++++++++ 5 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 frontend/src/utils/tradeListSort.ts diff --git a/app/src/screens/virtual/VirtualHistoryScreen.tsx b/app/src/screens/virtual/VirtualHistoryScreen.tsx index d395623..464ea11 100644 --- a/app/src/screens/virtual/VirtualHistoryScreen.tsx +++ b/app/src/screens/virtual/VirtualHistoryScreen.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { PaperTradeDto } from '../../lib/shared'; +import { compareTimeAsc } from '@frontend/utils/tradeListSort'; import MobileStackHeader from '../../components/MobileStackHeader'; interface Props { @@ -9,7 +10,13 @@ interface Props { } export default function VirtualHistoryScreen({ market, trades, onBack }: Props) { - const filtered = trades.filter(t => t.symbol === market).slice(0, 50); + const filtered = useMemo( + () => trades + .filter(t => t.symbol === market) + .sort((a, b) => compareTimeAsc(a.createdAt, b.createdAt)) + .slice(0, 50), + [trades, market], + ); return (
diff --git a/frontend/src/components/TradeNotificationListPage.tsx b/frontend/src/components/TradeNotificationListPage.tsx index 14fd0b8..f1fea82 100644 --- a/frontend/src/components/TradeNotificationListPage.tsx +++ b/frontend/src/components/TradeNotificationListPage.tsx @@ -45,6 +45,7 @@ import { getTradeNotificationGridPreset, type TradeNotificationGridPresetId, } from '../utils/tradeNotificationGridPresets'; +import { sortTradeNotifications } from '../utils/tradeListSort'; type RightTab = 'trade' | 'orderbook'; @@ -88,24 +89,6 @@ function fillBothSides( setFillSell({ market, price, side: 'sell', seq: seq + 1 }); } -function sortNotifications( - items: TradeNotificationItem[], - sort: TradeNotificationListSort, -): TradeNotificationItem[] { - const arr = [...items]; - if (sort === 'market') { - return arr.sort((a, b) => { - const byMarket = a.market.localeCompare(b.market, 'ko'); - if (byMarket !== 0) return byMarket; - return b.receivedAt - a.receivedAt; - }); - } - if (sort === 'oldest') { - return arr.sort((a, b) => a.receivedAt - b.receivedAt); - } - return arr.sort((a, b) => b.receivedAt - a.receivedAt); -} - export const TradeNotificationListPage: React.FC = ({ theme, onGoToChart, @@ -273,7 +256,7 @@ export const TradeNotificationListPage: React.FC = ({ }, [allNotifications, filter, candleFilter]); const sorted = useMemo( - () => sortNotifications(filtered, listSort), + () => sortTradeNotifications(filtered, listSort), [filtered, listSort], ); diff --git a/frontend/src/components/backtest/BacktestExecutionList.tsx b/frontend/src/components/backtest/BacktestExecutionList.tsx index 8246fa9..b72751f 100644 --- a/frontend/src/components/backtest/BacktestExecutionList.tsx +++ b/frontend/src/components/backtest/BacktestExecutionList.tsx @@ -13,6 +13,7 @@ import { toUpbitMarket, } from '../../utils/backtestUiUtils'; import { getKoreanName } from '../../utils/marketNameCache'; +import { compareMarketSymbol, compareTimeAsc } from '../../utils/tradeListSort'; export type ExecutionListTab = 'backtest' | 'live'; export type BacktestListSort = 'strategy' | 'symbol' | 'return'; @@ -58,7 +59,13 @@ export default function BacktestExecutionList({ const sortedBacktests = useMemo(() => { const list = [...backtestRecords]; if (sort === 'strategy') list.sort((a, b) => (a.strategyName ?? '').localeCompare(b.strategyName ?? '')); - else if (sort === 'symbol') list.sort((a, b) => a.symbol.localeCompare(b.symbol)); + else if (sort === 'symbol') { + list.sort((a, b) => { + const bySymbol = compareMarketSymbol(a.symbol, b.symbol); + if (bySymbol !== 0) return bySymbol; + return compareTimeAsc(a.createdAt, b.createdAt); + }); + } else list.sort((a, b) => (b.totalReturn ?? 0) - (a.totalReturn ?? 0)); return list; }, [backtestRecords, sort]); @@ -117,7 +124,13 @@ export default function BacktestExecutionList({

가상투자 화면에서 자동·수동
매매를 실행하세요.

) : ( - liveItems.map(item => { + [...liveItems] + .sort((a, b) => { + const bySymbol = compareMarketSymbol(a.symbol, b.symbol); + if (bySymbol !== 0) return bySymbol; + return compareTimeAsc(a.createdAt, b.createdAt); + }) + .map(item => { const positive = item.totalReturnPct >= 0; const active = selectedLiveId === item.id; const market = toUpbitMarket(item.symbol); diff --git a/frontend/src/components/paper/PaperTradeHistoryList.tsx b/frontend/src/components/paper/PaperTradeHistoryList.tsx index fe66707..b585d5c 100644 --- a/frontend/src/components/paper/PaperTradeHistoryList.tsx +++ b/frontend/src/components/paper/PaperTradeHistoryList.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { PaperTradeDto } from '../../utils/backendApi'; import type { TickerData } from '../../hooks/useMarketTicker'; import { resolveVirtualTargetNames } from '../../utils/virtualTargetNames'; import { fmtKrw } from '../TradeOrderPanel'; +import { sortPaperTradesForDisplay } from '../../utils/tradeListSort'; function coinCode(symbol: string): string { return symbol.replace(/^KRW-/, ''); @@ -43,7 +44,10 @@ const PaperTradeHistoryList: React.FC = ({ selectedTradeId = null, onSelectTrade, onSelectMarket, -}) => ( +}) => { + const displayTrades = useMemo(() => sortPaperTradesForDisplay(trades), [trades]); + + return (
@@ -56,7 +60,7 @@ const PaperTradeHistoryList: React.FC = ({ ) : (
- {trades.map(t => { + {displayTrades.map(t => { const { koreanName: ko, englishName: en } = resolveVirtualTargetNames( t.symbol, tickers?.get(t.symbol)?.koreanName, @@ -164,6 +168,7 @@ const PaperTradeHistoryList: React.FC = ({ )}
-); + ); +}; export default PaperTradeHistoryList; diff --git a/frontend/src/utils/tradeListSort.ts b/frontend/src/utils/tradeListSort.ts new file mode 100644 index 0000000..c0de24f --- /dev/null +++ b/frontend/src/utils/tradeListSort.ts @@ -0,0 +1,57 @@ +import type { PaperTradeDto } from './backendApi'; +import type { TradeNotificationItem } from '../contexts/TradeNotificationContext'; +import type { TradeNotificationListSort } from './tradeNotificationListLayout'; + +/** 종목(마켓) 코드 비교 — 한글 정렬 */ +export function compareMarketSymbol(a: string, b: string): number { + return a.localeCompare(b, 'ko'); +} + +/** epoch ms·ISO 문자열·초 단위 타임스탬프 → 오름차순(이전 → 최근) */ +export function compareTimeAsc( + a: number | string | null | undefined, + b: number | string | null | undefined, +): number { + return toTimeMs(a) - toTimeMs(b); +} + +function toTimeMs(t: number | string | null | undefined): number { + if (t == null) return 0; + if (typeof t === 'number') { + if (!Number.isFinite(t)) return 0; + return t < 1e12 ? t * 1000 : t; + } + const d = new Date(t); + return Number.isNaN(d.getTime()) ? 0 : d.getTime(); +} + +/** 종목순 → 동일 종목은 체결·수신 시각 오름차순 (매수·매도 흐름) */ +export function sortByMarketThenTimeAsc( + items: readonly T[], + getMarket: (item: T) => string, + getTime: (item: T) => number | string | null | undefined, +): T[] { + return [...items].sort((a, b) => { + const byMarket = compareMarketSymbol(getMarket(a), getMarket(b)); + if (byMarket !== 0) return byMarket; + return compareTimeAsc(getTime(a), getTime(b)); + }); +} + +export function sortPaperTradesForDisplay(trades: readonly PaperTradeDto[]): PaperTradeDto[] { + return sortByMarketThenTimeAsc(trades, t => t.symbol, t => t.createdAt); +} + +export function sortTradeNotifications( + items: TradeNotificationItem[], + sort: TradeNotificationListSort, +): TradeNotificationItem[] { + const arr = [...items]; + if (sort === 'market') { + return sortByMarketThenTimeAsc(arr, n => n.market, n => n.receivedAt); + } + if (sort === 'oldest') { + return arr.sort((a, b) => compareTimeAsc(a.receivedAt, b.receivedAt)); + } + return arr.sort((a, b) => compareTimeAsc(b.receivedAt, a.receivedAt)); +}