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

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
+32 -15
View File
@@ -8,7 +8,7 @@
* 관심 - 즐겨찾기 목록 (localStorage 관리)
*/
import React, { useState, useCallback, useMemo, useRef, useEffect, memo } from 'react';
import type { TickerData, ChangeDir } from '../hooks/useMarketTicker';
import type { TickerData, ChangeDir, MarketInfo } from '../hooks/useMarketTicker';
import {
getFavorites, saveFavorites, toggleFavorite,
getHoldings, addHolding, removeHolding,
@@ -22,6 +22,8 @@ type SortDir = 'asc' | 'desc';
export interface MarketPanelProps {
open: boolean;
tickers: Map<string, TickerData>;
/** 업비트 KRW 전 종목 (종목검색 드롭다운과 동일 소스) */
marketInfos: MarketInfo[];
loading: boolean;
usdRate: number; // KRW per 1 USD (from KRW-USDT)
activeSymbol: string;
@@ -59,6 +61,7 @@ const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir })
interface RowProps {
ticker: TickerData | undefined;
market: string;
koreanName: string;
isActive: boolean;
isFav: boolean;
isHold: boolean;
@@ -72,7 +75,7 @@ interface RowProps {
}
const MarketRow = memo(function MarketRow({
ticker, market, isActive, isFav, isHold,
ticker, market, koreanName, isActive, isFav, isHold,
showUsd, showHoldBtn, usdRate,
onSelect, onToggleFav, onToggleHold, onRemoveHold,
}: RowProps) {
@@ -116,7 +119,7 @@ const MarketRow = memo(function MarketRow({
<div className="mp-name">
<span className="mp-korean-row">
<ChangeArrow change={change} />
<span className="mp-korean">{ticker?.koreanName ?? code}</span>
<span className="mp-korean">{koreanName}</span>
</span>
<span className="mp-code">{code}/KRW</span>
</div>
@@ -151,7 +154,7 @@ const MarketRow = memo(function MarketRow({
// ─── 메인 패널 ─────────────────────────────────────────────────────────────
export const MarketPanel: React.FC<MarketPanelProps> = ({
open, tickers, loading, usdRate, activeSymbol, onSymbolSelect,
open, tickers, marketInfos, loading, usdRate, activeSymbol, onSymbolSelect,
}) => {
const [tab, setTab] = useState<Tab>('krw');
const [search, setSearch] = useState('');
@@ -214,14 +217,27 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
setShowHoldAdd(false);
}, []);
// 탭별 마켓 목록 계산
const koreanNameByMarket = useMemo(() => {
const map = new Map<string, string>();
marketInfos.forEach(m => map.set(m.market, m.koreanName));
return map;
}, [marketInfos]);
const resolveKoreanName = useCallback((market: string) =>
tickers.get(market)?.koreanName
?? koreanNameByMarket.get(market)
?? market.replace(/^KRW-/, ''),
[tickers, koreanNameByMarket]);
// 탭별 마켓 목록 — market/all 전 종목 (종목검색 드롭다운과 동일)
const allKrwMarkets = useMemo(() => {
const keys = Array.from(tickers.keys()).filter(m => m.startsWith('KRW-'));
// 24h 거래대금 내림차순 정렬
const keys = marketInfos.length > 0
? marketInfos.map(m => m.market)
: Array.from(tickers.keys()).filter(m => m.startsWith('KRW-'));
return keys.sort((a, b) =>
(tickers.get(b)?.accTradePrice24 ?? 0) - (tickers.get(a)?.accTradePrice24 ?? 0)
);
}, [tickers]);
}, [marketInfos, tickers]);
const filtered = useMemo(() => {
let base: string[] = [];
@@ -234,7 +250,7 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
const q = search.toUpperCase();
base = base.filter(m => {
const code = m.replace(/^KRW-/, '');
const name = tickers.get(m)?.koreanName ?? '';
const name = resolveKoreanName(m);
return code.includes(q) || name.includes(search);
});
}
@@ -245,8 +261,8 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
const tb = tickers.get(b);
switch (sortKey) {
case 'name': {
const na = ta?.koreanName ?? a.replace(/^KRW-/, '');
const nb = tb?.koreanName ?? b.replace(/^KRW-/, '');
const na = resolveKoreanName(a);
const nb = resolveKoreanName(b);
return mul * na.localeCompare(nb, 'ko');
}
case 'price':
@@ -257,7 +273,7 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
return 0;
}
});
}, [tab, allKrwMarkets, holdings, favorites, search, tickers, sortKey, sortDir]);
}, [tab, allKrwMarkets, holdings, favorites, search, tickers, sortKey, sortDir, resolveKoreanName]);
// 보유 탭 "추가" 검색 결과
const holdAddResults = useMemo(() => {
@@ -266,11 +282,11 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
return allKrwMarkets
.filter(m => {
const code = m.replace(/^KRW-/, '');
const name = tickers.get(m)?.koreanName ?? '';
const name = resolveKoreanName(m);
return code.includes(q) || name.includes(holdSearch);
})
.slice(0, 10);
}, [holdSearch, allKrwMarkets, tickers]);
}, [holdSearch, allKrwMarkets, resolveKoreanName]);
const tabLabels: Record<Tab, string> = {
krw: '원화', btc: 'BTC', hold: '보유', fav: '관심',
@@ -352,7 +368,7 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
<div className="mp-hold-add-dropdown">
{holdAddResults.map(m => (
<div key={m} className="mp-hold-add-item" onClick={() => handleAddHolding(m)}>
<span>{tickers.get(m)?.koreanName ?? m.replace(/^KRW-/, '')}</span>
<span>{resolveKoreanName(m)}</span>
<span className="mp-hold-add-code">{m.replace(/^KRW-/, '')}</span>
</div>
))}
@@ -388,6 +404,7 @@ export const MarketPanel: React.FC<MarketPanelProps> = ({
<MarketRow
key={market}
market={market}
koreanName={resolveKoreanName(market)}
ticker={tickers.get(market)}
isActive={market === activeSymbol}
isFav={favorites.includes(market)}