전략편집기 템플릿 저장 기능 추가
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { setMarketNames } from '../utils/marketNameCache';
|
||||
import {
|
||||
getFavorites,
|
||||
@@ -45,6 +45,9 @@ function formatChangeRate(raw: number | string | null | undefined): number | nul
|
||||
return rate * 100;
|
||||
}
|
||||
|
||||
type SortKey = 'name' | 'price' | 'change' | 'volume';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
interface MarketSearchPanelProps {
|
||||
currentMarket: string;
|
||||
@@ -94,6 +97,8 @@ export const MarketSearchPanel: React.FC<MarketSearchPanelProps> = ({
|
||||
}, [controlledQuery, onQueryChange]);
|
||||
const [favs, setFavs] = useState<Set<string>>(() => new Set(getFavorites()));
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sortKey, setSortKey] = useState<SortKey | null>(null);
|
||||
const [sortDir, setSortDir] = useState<SortDir>('desc');
|
||||
|
||||
const refreshFavs = useCallback(() => {
|
||||
setFavs(new Set(getFavorites()));
|
||||
@@ -215,6 +220,32 @@ export const MarketSearchPanel: React.FC<MarketSearchPanelProps> = ({
|
||||
tryAddMarket(market);
|
||||
}, [tryAddMarket]);
|
||||
|
||||
const handleSort = useCallback((key: SortKey) => {
|
||||
setSortKey(prev => {
|
||||
if (prev === key) {
|
||||
setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||
return key;
|
||||
}
|
||||
setSortDir('desc');
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderSortHeader = (key: SortKey, label: string, className: string) => {
|
||||
const active = sortKey === key;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`msp-col ${className} msp-h-sort-btn ${active ? 'msp-h-sort-btn--active' : ''}`}
|
||||
onClick={() => handleSort(key)}
|
||||
title={active ? (sortDir === 'asc' ? '내림차순 정렬' : '오름차순 정렬') : `${label} 정렬`}
|
||||
>
|
||||
{label}
|
||||
<span className="msp-sort-icon">{active ? (sortDir === 'asc' ? '▲' : '▼') : '⇅'}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// ── 4. 필터 + 정렬 ───────────────────────────────────────────────────────
|
||||
const q = query.trim();
|
||||
const filtered = markets.filter(m => {
|
||||
@@ -227,15 +258,42 @@ export const MarketSearchPanel: React.FC<MarketSearchPanelProps> = ({
|
||||
);
|
||||
});
|
||||
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
const fa = favs.has(a.market) ? 1 : 0;
|
||||
const fb = favs.has(b.market) ? 1 : 0;
|
||||
if (fb !== fa) return fb - fa;
|
||||
// 즐겨찾기 동순위: 24h 거래대금 내림차순
|
||||
const va = tickers[a.market]?.acc_trade_price_24h ?? 0;
|
||||
const vb = tickers[b.market]?.acc_trade_price_24h ?? 0;
|
||||
return vb - va;
|
||||
});
|
||||
const sorted = useMemo(() => {
|
||||
if (!sortKey) {
|
||||
return [...filtered].sort((a, b) => {
|
||||
const fa = favs.has(a.market) ? 1 : 0;
|
||||
const fb = favs.has(b.market) ? 1 : 0;
|
||||
if (fb !== fa) return fb - fa;
|
||||
const va = tickers[a.market]?.acc_trade_price_24h ?? 0;
|
||||
const vb = tickers[b.market]?.acc_trade_price_24h ?? 0;
|
||||
return vb - va;
|
||||
});
|
||||
}
|
||||
const mul = sortDir === 'asc' ? 1 : -1;
|
||||
return [...filtered].sort((a, b) => {
|
||||
switch (sortKey) {
|
||||
case 'name':
|
||||
return mul * a.korean_name.localeCompare(b.korean_name, 'ko');
|
||||
case 'price': {
|
||||
const pa = tickers[a.market]?.trade_price ?? 0;
|
||||
const pb = tickers[b.market]?.trade_price ?? 0;
|
||||
return mul * (pa - pb);
|
||||
}
|
||||
case 'change': {
|
||||
const ca = tickers[a.market]?.signed_change_rate ?? 0;
|
||||
const cb = tickers[b.market]?.signed_change_rate ?? 0;
|
||||
return mul * (ca - cb);
|
||||
}
|
||||
case 'volume': {
|
||||
const va = tickers[a.market]?.acc_trade_price_24h ?? 0;
|
||||
const vb = tickers[b.market]?.acc_trade_price_24h ?? 0;
|
||||
return mul * (va - vb);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}, [filtered, sortKey, sortDir, favs, tickers]);
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
// anchorRect 제공 시: 슬롯 영역 안에 패널을 위치시킨다.
|
||||
@@ -362,14 +420,12 @@ export const MarketSearchPanel: React.FC<MarketSearchPanelProps> = ({
|
||||
{!isCompactDropdown && (
|
||||
<span className="msp-col msp-col-fav">{embedded ? '추가' : ''}</span>
|
||||
)}
|
||||
<span className="msp-col msp-col-name">{isCompactDropdown ? '종목명' : '한글명'}</span>
|
||||
{renderSortHeader('name', isCompactDropdown ? '종목명' : '한글명', 'msp-col-name')}
|
||||
{!embedded && (
|
||||
<>
|
||||
<span className="msp-col msp-col-price">현재가</span>
|
||||
<span className="msp-col msp-col-change">전일대비</span>
|
||||
{!isCompactDropdown && (
|
||||
<span className="msp-col msp-col-vol">거래대금</span>
|
||||
)}
|
||||
{renderSortHeader('price', '현재가', 'msp-col-price')}
|
||||
{renderSortHeader('change', '전일대비', 'msp-col-change')}
|
||||
{!isCompactDropdown && renderSortHeader('volume', '거래대금', 'msp-col-vol')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user