From c63775bba643ca547c9e3c51d2ec390cd74d9148 Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 23 Jun 2026 00:35:56 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=20=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=EC=9D=B4=EC=A0=84=EC=A2=85=EB=AA=A9,=20=EB=8B=A4=EC=9D=8C?= =?UTF-8?q?=EC=A2=85=EB=AA=A9=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MarketSearchPanel.tsx | 123 ++++++------------ .../src/components/StrategyEvaluationPage.tsx | 4 +- .../StrategyEvaluationChart.tsx | 78 +++++++++++ frontend/src/styles/strategyEvaluation.css | 27 ++++ frontend/src/utils/marketSearchList.ts | 117 +++++++++++++++++ 5 files changed, 262 insertions(+), 87 deletions(-) create mode 100644 frontend/src/utils/marketSearchList.ts diff --git a/frontend/src/components/MarketSearchPanel.tsx b/frontend/src/components/MarketSearchPanel.tsx index 69f2dc2..e3a3485 100644 --- a/frontend/src/components/MarketSearchPanel.tsx +++ b/frontend/src/components/MarketSearchPanel.tsx @@ -12,6 +12,13 @@ import { import { useIsCompactDevice } from '../hooks/useMediaQuery'; import { safeToFixed } from '../utils/safeFormat'; import { VirtualScroll } from './common/VirtualScroll'; +import { + buildSortedMarketRows, + filterMarketRows, + marketInfosToRows, + type MarketSortDir, + type MarketSortKey, +} from '../utils/marketSearchList'; // ─── Types ─────────────────────────────────────────────────────────────────── interface MarketRow { @@ -30,29 +37,10 @@ function formatVol(vol: number | string | null | undefined): string { return v.toLocaleString(); } -function compareNullableNum( - a: number | null | undefined, - b: number | null | undefined, - mul: number, -): number { - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - return mul * (a - b); -} - function hookInfosToRows(infos: HookMarketInfo[]): MarketRow[] { - return infos.map(m => ({ - market: m.market, - korean_name: m.koreanName, - english_name: m.englishName, - symbol: m.market.replace('KRW-', ''), - })); + return marketInfosToRows(infos) as MarketRow[]; } -type SortKey = 'name' | 'price' | 'change' | 'volume'; -type SortDir = 'asc' | 'desc'; - // ─── Component ─────────────────────────────────────────────────────────────── interface MarketSearchPanelProps { currentMarket: string; @@ -91,6 +79,11 @@ interface MarketSearchPanelProps { tickerLoadFull?: boolean; /** 내부 훅 우선 종목 (전략평가 차트 종목 등) */ tickerPriorityMarkets?: string[]; + /** 정렬 상태 공유 (전략평가 이전/다음 종목 등) */ + sortKey?: MarketSortKey | null; + sortDir?: MarketSortDir; + onSortKeyChange?: (key: MarketSortKey | null) => void; + onSortDirChange?: (dir: MarketSortDir) => void; } export const MarketSearchPanel: React.FC = ({ @@ -99,6 +92,7 @@ export const MarketSearchPanel: React.FC = ({ actionMode = 'favorite', onAdd, onRemove, addedMarkets, maxTargetsReached = false, tickers: tickersProp, marketInfos: marketInfosProp, marketLoading: marketLoadingProp, refreshAllTickers: refreshProp, tickerLoadFull = true, tickerPriorityMarkets, + sortKey: sortKeyProp, sortDir: sortDirProp, onSortKeyChange, onSortDirChange, }) => { const embedded = variant === 'embedded'; const isCompact = useIsCompactDevice(); @@ -124,8 +118,12 @@ export const MarketSearchPanel: React.FC = ({ onQueryChange?.(next); }, [controlledQuery, onQueryChange]); const [favs, setFavs] = useState>(() => new Set(getFavorites())); - const [sortKey, setSortKey] = useState(null); - const [sortDir, setSortDir] = useState('desc'); + const [internalSortKey, setInternalSortKey] = useState(null); + const [internalSortDir, setInternalSortDir] = useState('desc'); + const sortKey = sortKeyProp !== undefined ? sortKeyProp : internalSortKey; + const sortDir = sortDirProp ?? internalSortDir; + const setSortKey = onSortKeyChange ?? setInternalSortKey; + const setSortDir = onSortDirChange ?? setInternalSortDir; const refreshFavs = useCallback(() => { setFavs(new Set(getFavorites())); @@ -193,27 +191,24 @@ export const MarketSearchPanel: React.FC = ({ tryAddMarket(market); }, [tryAddMarket]); - const applySortState = useCallback((key: SortKey) => { - setSortKey(prev => { - if (prev === key) { - setSortDir(d => d === 'asc' ? 'desc' : 'asc'); - return key; - } - setSortDir('desc'); - return key; - }); - }, []); - - const handleSort = useCallback((key: SortKey) => { - const needsRefresh = key === 'change' || key === 'price' || key === 'volume'; - if (needsRefresh) { - void refreshAllTickers().then(() => applySortState(key)); + const applySortState = useCallback((key: MarketSortKey) => { + if (sortKey === key) { + setSortDir(sortDir === 'asc' ? 'desc' : 'asc'); return; } + setSortDir('desc'); + setSortKey(key); + }, [sortKey, sortDir, setSortKey, setSortDir]); + + const handleSort = useCallback((key: MarketSortKey) => { applySortState(key); + const needsRefresh = key === 'change' || key === 'price' || key === 'volume'; + if (needsRefresh) { + void refreshAllTickers(); + } }, [refreshAllTickers, applySortState]); - const renderSortHeader = (key: SortKey, label: string, className: string) => { + const renderSortHeader = (key: MarketSortKey, label: string, className: string) => { const active = sortKey === key; return ( + +