import React, { useRef, useEffect, useState, useCallback } from 'react'; import ReactDOM from 'react-dom'; import IndicatorDropdownPanel from './IndicatorDropdownPanel'; import type { ChartType, Theme, Timeframe, ChartMode, IndicatorConfig } from '../types'; import { MarketSearchPanel } from './MarketSearchPanel'; import { getKoreanName } from '../utils/marketNameCache'; import { formatChartLiveChangePct, formatChartLivePrice, type ChartLiveQuote, } from '../utils/chartLiveQuote'; import LayoutPicker from './LayoutPicker'; import TradeAlertPopupMenubarSelect from './TradeAlertPopupMenubarSelect'; import { DEFAULT_SYNC, type LayoutDef, type SyncOptions } from '../utils/layoutTypes'; import { loadStrategies } from '../utils/backendApi'; import type { TickerData, MarketInfo } from '../hooks/useMarketTicker'; interface BtStrategy { id?: number; name: string; source: 'db'; buyCondition?: unknown; sellCondition?: unknown; } function loadBtStrategies(): Promise { return loadStrategies() .then(list => (list ?? []).map(s => ({ id: s.id, name: s.name, source: 'db' as const, buyCondition: s.buyCondition, sellCondition: s.sellCondition, }))) .catch(() => []); } export interface ToolbarProps { symbol: string; timeframe: Timeframe; chartType: ChartType; theme: Theme; mode: ChartMode; logScale: boolean; showGrid: boolean; activeIndicators: IndicatorConfig[]; onSymbol: (s: string) => void; onTimeframe: (t: Timeframe) => void; onChartType: (t: ChartType) => void; onTheme: () => void; onMode: () => void; onLogScale: () => void; onToggleGrid: () => void; onFitContent: () => void; onScrollToNow?: () => void; onAddIndicator: (type: string) => void; onAddIndicators?: (types: string[]) => void; /** 탭 목록 순서 변경 → 차트 pane 순서 동기화 */ onIndicatorOrderChange?: (orderedTypes: string[]) => void; onRemoveByType: (type: string) => void; onAutoFib: () => void; onClearFib: () => void; onScreenshot: () => void; onToggleStats: () => void; onToggleWatch: () => void; onToggleAlert: () => void; /** 매매 시그널 알림 (미확인 개수) */ tradeNotifyUnread?: number; onOpenTradeNotifications?: () => void; /** 자석모드 현재 상태 */ magnetMode?: 'off' | 'weak' | 'strong'; /** 자석모드 토글 (off↔strong) */ onToggleMagnet?: () => void; onUndo?: () => void; onRedo?: () => void; canUndo?: boolean; canRedo?: boolean; onClearDrawings?: () => void; layoutId?: string; syncOptions?: SyncOptions; onLayoutSelect?: (def: LayoutDef) => void; onSyncChange?: (key: keyof SyncOptions, val: boolean) => void; /** 돋보기(확대경) 활성 여부 */ magnifierActive?: boolean; /** 돋보기 토글 */ onToggleMagnifier?: () => void; /** 좌측 마켓 패널 활성 여부 */ marketPanelActive?: boolean; /** 마켓 패널 토글 */ onToggleMarketPanel?: () => void; /** useMarketTicker 공유 시세 (종목검색·마켓패널 통합) */ marketTickers?: Map; marketInfos?: MarketInfo[]; marketLoading?: boolean; refreshAllTickers?: () => Promise; onMarketSearchOpenChange?: (open: boolean) => void; /** 백테스팅 드롭다운 표시 여부 */ backtestActive?: boolean; /** 백테스팅 실행 콜백 */ onRunBacktest?: (opts: { strategyId?: number; buyCondition?: unknown; sellCondition?: unknown; strategyName?: string }) => void; /** 백테스팅 실행 중 여부 */ backtestRunning?: boolean; /** 현재 실행 중인 전략 ID */ backtestRunningId?: number | string; /** 백테스팅 설정 버튼 클릭 콜백 */ onOpenBtSettings?: () => void; /** 백테스팅 결과보기 버튼 클릭 콜백 */ onOpenBtResults?: () => void; /** 현재 실행된 결과 존재 여부 (결과보기 버튼 강조) */ hasBtResult?: boolean; /** 매수/매도 마커 삭제 콜백 */ onClearBtMarkers?: () => void; /** 실시간 전략 체크 패널 토글 콜백 */ onToggleLiveStrategy?: () => void; /** 실시간 전략 패널 열림 여부 */ liveStrategyActive?: boolean; /** 보조지표 일괄 설정 팝업 */ onOpenBulkIndicatorSettings?: () => void; /** 지표 추가 팝업 항목 — 해당 지표 설정 모달 */ onOpenIndicatorSettings?: (type: string) => void; /** 종목 선택 우측 시세 (실시간 차트 — 하단 upbit-bar 대체) */ showChartQuote?: boolean; chartLiveQuote?: ChartLiveQuote | null; chartQuoteLoading?: boolean; chartQuoteError?: string | null; /** 9·20일 신고가·신저가 차트 표시 (실시간 차트 상단 툴바) */ priceExtremeLabelsVisible?: boolean; onTogglePriceExtremeLabels?: () => void; /** 매매 알림 팝업 표시 위치·방식 */ tradeAlertPopupPosition?: string; tradeAlertPopupLayout?: string; tradeAlertPopupGridCols?: number; onTradeAlertPopupPosition?: (v: import('../utils/tradeAlertPopupLayout').TradeAlertPopupPosition) => void; onTradeAlertPopupLayout?: (v: import('../utils/tradeAlertPopupLayout').TradeAlertPopupLayout) => void; onTradeAlertPopupGridCols?: (v: number) => void; } // ─── SVG 아이콘 ──────────────────────────────────────────────────────────── const IcSearch = () => ( ); const IcPlus = () => ( ); const IcCandlestick = () => ( ); const IcLine = () => ( ); const IcArea = () => ( ); const IcBar = () => ( ); const IcIndicator = () => ( ); /** 보조지표 일괄 설정 (막대 + 슬라이더) */ const IcIndBulkSettings = () => ( ); const IcUndo = () => ( ); const IcRedo = () => ( ); const IcCamera = () => ( ); const IcBell = () => ( ); /** 매매 시그널 알림 (번개 아이콘으로 가격 알림 벨과 구분) */ const IcTradeSignal = () => ( ); const IcPriceExtreme = () => ( ); const IcGridIcon = () => ( ); const IcStats = () => ( ); const IcFibAuto = () => ( ); const IcMagnet = () => ( ); const IcList = () => ( ); const IcReplay = () => ( ); // ─── 차트 타입 아이콘 매핑 ───────────────────────────────────────────────── const ChartIcon: Record = { candlestick: , bar: , line: , area: , baseline: , }; const TIMEFRAMES: Timeframe[] = ['1m','3m','5m','10m','15m','30m','1h','4h','1D','1W','1M']; const CHART_TYPES: { value: ChartType; label: string }[] = [ { value: 'candlestick', label: '캔들스틱' }, { value: 'bar', label: '바' }, { value: 'line', label: '라인' }, { value: 'area', label: '영역' }, { value: 'baseline', label: '기준선' }, ]; // ─── Toolbar ────────────────────────────────────────────────────────────── const Toolbar: React.FC = ({ symbol, timeframe, chartType, theme, mode, logScale, showGrid, activeIndicators, onSymbol, onTimeframe, onChartType, onTheme, onMode, onFitContent, onScrollToNow, onAddIndicator, onAddIndicators, onIndicatorOrderChange, onRemoveByType, onAutoFib, onClearFib, onScreenshot, onToggleStats, onToggleWatch, onToggleAlert, tradeNotifyUnread = 0, onOpenTradeNotifications, magnetMode = 'off', onToggleMagnet, onUndo, onRedo, canUndo, canRedo, onClearDrawings, onToggleGrid, layoutId = '1', syncOptions = DEFAULT_SYNC, onLayoutSelect, onSyncChange, magnifierActive = false, onToggleMagnifier, marketPanelActive = false, onToggleMarketPanel, marketTickers, marketInfos, marketLoading, refreshAllTickers, onMarketSearchOpenChange, backtestActive = false, onRunBacktest, backtestRunning = false, backtestRunningId, onOpenBtSettings, onOpenBtResults, hasBtResult = false, onClearBtMarkers, onToggleLiveStrategy, liveStrategyActive = false, onOpenBulkIndicatorSettings, onOpenIndicatorSettings, showChartQuote = false, chartLiveQuote = null, chartQuoteLoading = false, chartQuoteError = null, priceExtremeLabelsVisible = false, onTogglePriceExtremeLabels, tradeAlertPopupPosition, tradeAlertPopupLayout, tradeAlertPopupGridCols, onTradeAlertPopupPosition, onTradeAlertPopupLayout, onTradeAlertPopupGridCols, }) => { const [showMarket, setShowMarket] = React.useState(false); const [showTfMenu, setShowTfMenu] = React.useState(false); const [showCtMenu, setShowCtMenu] = React.useState(false); const [showIndMenu,setShowIndMenu]= React.useState(false); const [showLayout, setShowLayout] = useState(false); const [layoutPopupPos, setLayoutPopupPos] = useState<{ top: number; right: number } | null>(null); const layoutBtnRef = useRef(null); // 백테스팅 드롭다운 const [showBtDrop, setShowBtDrop] = useState(false); const [btStrategies, setBtStrategies] = useState([]); const [btLoading, setBtLoading] = useState(false); const btBtnRef = useRef(null); const btDropRef = useRef(null); const openBtDrop = useCallback(async () => { if (showBtDrop) { setShowBtDrop(false); return; } setBtLoading(true); setShowBtDrop(true); try { const list = await loadBtStrategies(); setBtStrategies(list); } finally { setBtLoading(false); } }, [showBtDrop]); // 바깥 클릭 시 닫기 useEffect(() => { if (!showBtDrop) return; const handler = (e: MouseEvent) => { if ( btBtnRef.current && btBtnRef.current.contains(e.target as Node) || btDropRef.current && btDropRef.current.contains(e.target as Node) ) return; setShowBtDrop(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [showBtDrop]); const handleLayoutBtnClick = useCallback(() => { if (showLayout) { setShowLayout(false); setLayoutPopupPos(null); return; } const rect = layoutBtnRef.current?.getBoundingClientRect(); if (rect) { setLayoutPopupPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right, }); } setShowLayout(true); }, [showLayout]); const openMarketSearch = useCallback(() => { setShowMarket(v => { const next = !v; onMarketSearchOpenChange?.(next); if (next) void refreshAllTickers?.(); return next; }); setShowTfMenu(false); setShowCtMenu(false); setShowIndMenu(false); }, [onMarketSearchOpenChange, refreshAllTickers]); const closeMarketSearch = useCallback(() => { setShowMarket(false); onMarketSearchOpenChange?.(false); }, [onMarketSearchOpenChange]); return ( <> {/* 종목 검색 패널 */} {showMarket && ( { onSymbol(s); closeMarketSearch(); }} onClose={closeMarketSearch} tickers={marketTickers} marketInfos={marketInfos} marketLoading={marketLoading} refreshAllTickers={refreshAllTickers} /> )}
{/* ── Left ─────────────────────────────────────────────────────────── */}
{/* 종목 검색(아이콘) · 선택 종목 표시 — 클릭 시 MarketSearchPanel */}
{symbol} {showChartQuote && chartQuoteLoading && ( 데이터 로딩 중… )} {showChartQuote && chartQuoteError && ( ⚠ {chartQuoteError} )} {showChartQuote && !chartQuoteLoading && !chartQuoteError && chartLiveQuote?.price != null && ( <> {formatChartLivePrice(chartLiveQuote.price)} {chartLiveQuote.changePct != null && ( {formatChartLiveChangePct(chartLiveQuote.changePct)} )} )}
{/* Compare */}
{/* Timeframe Dropdown */}
{showTfMenu && (
setShowTfMenu(false)}> {TIMEFRAMES.map(tf => ( ))}
)}
{/* Chart Type Dropdown */}
{showCtMenu && (
setShowCtMenu(false)}> {CHART_TYPES.map(ct => ( ))}
)}
{/* Indicators */} {/* 지표 패널은 fixed overlay로 렌더링 — 부모 overflow 영향 없음 */} {showIndMenu && ( onAddIndicator(type)} onAddMany={onAddIndicators} onIndicatorOrderChange={onIndicatorOrderChange} onRemove={type => onRemoveByType(type)} onOpenSettings={onOpenIndicatorSettings} onClose={() => setShowIndMenu(false)} /> )} {onOpenBulkIndicatorSettings && ( )}
{/* Undo / Redo */}
{/* 자석모드 토글 */} {/* 전체 드로잉 삭제 */} {/* 통계 패널 */} {/* Auto Fibonacci */} {/* 9·20일 신고가·신저가 표시 */} {onTogglePriceExtremeLabels && ( )} {/* 그리드 토글 */}
{/* Mode toggle */} {/* 백테스팅 드롭다운 버튼 + 설정 버튼 */} {onRunBacktest && (
{showBtDrop && (
전략 목록
{btLoading ? (
로딩 중…
) : btStrategies.length === 0 ? (
저장된 전략이 없습니다
투자전략 화면에서 전략을 추가하세요
) : (
    {btStrategies.map((s, i) => { const runId = s.id ?? `local-${i}`; const isRunning = backtestRunning && backtestRunningId === runId; const isDbId = s.id !== undefined && s.id > 0 && s.id < 1_000_000_000; return (
  • {s.name}
  • ); })}
)}
)}
{/* 백테스팅 설정 버튼 */} {onOpenBtSettings && ( )} {/* 결과보기 버튼 — 설정 버튼 우측 */} {onOpenBtResults && ( )} {/* 매수/매도 마커 삭제 버튼 — 결과보기 버튼 우측, 마커가 있을 때만 표시 */} {onClearBtMarkers && hasBtResult && ( )} {/* 실시간 전략 체크 패널 토글 버튼 */} {onToggleLiveStrategy && ( )}
)}
{/* ── Right ────────────────────────────────────────────────────────── */}
{/* 마켓 패널 토글 버튼 */} {onToggleMarketPanel && ( )} {/* 돋보기(확대경) 버튼 — 레이아웃 버튼 좌측 */} {onToggleMagnifier && ( )} {/* 레이아웃 선택 버튼 */} {/* Portal: 레이아웃 팝업을 body에 fixed 렌더 (toolbar overflow 클리핑 우회) */} {showLayout && layoutPopupPos && onLayoutSelect && onSyncChange && ReactDOM.createPortal(
{ onLayoutSelect(def); setShowLayout(false); setLayoutPopupPos(null); }} onSyncChange={onSyncChange} onClose={() => { setShowLayout(false); setLayoutPopupPos(null); }} />
, document.body, ) } {onScrollToNow && ( )} {onOpenTradeNotifications && ( <> {(onTradeAlertPopupPosition || onTradeAlertPopupLayout) && ( )} {tradeNotifyUnread > 0 && ( {tradeNotifyUnread > 99 ? '99+' : tradeNotifyUnread} )} )}
); }; export default Toolbar;