전략편집기 템플릿 저장 기능 추가

This commit is contained in:
Macbook
2026-06-11 23:24:29 +09:00
parent 05c15ec92b
commit eb6cf894da
9 changed files with 558 additions and 81 deletions
+72 -16
View File
@@ -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>