import React, { useMemo } from 'react'; import { getKoreanName } from '../../utils/marketNameCache'; import type { StrategyDto } from '../../utils/backendApi'; import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots'; import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash'; import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData'; import { buildConditionMetrics, resolveVirtualTradeTiming } from '../../utils/virtualSignalMetrics'; import VirtualLiveBadge from './VirtualLiveBadge'; import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus'; import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage'; import VirtualTargetCardChart from './VirtualTargetCardChart'; import VirtualTargetSignalPanel from './VirtualTargetSignalPanel'; import type { Theme } from '../../types'; import type { TickerData } from '../../hooks/useMarketTicker'; import VirtualTargetQuote from './VirtualTargetQuote'; export type VirtualCardDisplayMode = 'signal' | 'chart'; interface Props { market: string; strategy: StrategyDto | undefined; strategies: StrategyDto[]; strategyId: number | null; globalStrategyId: number | null; onStrategyChange?: (strategyId: number | null) => void; snapshot: VirtualIndicatorSnapshot | undefined; running: boolean; liveStatus?: VirtualLiveStatus; lastTickAt?: number; viewMode?: VirtualCardViewMode; displayMode?: VirtualCardDisplayMode; onDisplayModeChange?: (mode: VirtualCardDisplayMode) => void; onEnterFocus?: () => void; selected?: boolean; onSelect?: () => void; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; chartSeriesPriceLabels?: boolean; ticker?: TickerData; } const VirtualTargetCard: React.FC = ({ market, strategy, strategies, strategyId, globalStrategyId, onStrategyChange, snapshot, running, liveStatus = 'idle', lastTickAt, viewMode = 'summary', displayMode = 'signal', onDisplayModeChange, onEnterFocus, selected = false, onSelect, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', chartSeriesPriceLabels = true, ticker, }) => { const ko = getKoreanName(market); const sym = market.replace(/^KRW-/, ''); const tf = snapshot?.timeframe ?? '—'; const rows = snapshot?.rows ?? []; const metrics = useMemo(() => buildConditionMetrics(rows), [rows]); const tradeTiming = useMemo(() => resolveVirtualTradeTiming(metrics), [metrics]); const metCount = metrics.filter(m => m.status === 'match').length; const evalCount = metrics.filter(m => m.status !== 'unknown').length; const isDetail = viewMode === 'detail'; const isChart = displayMode === 'chart'; const receiveSignal = useMemo(() => { if (!running || isChart) return null; return `${lastTickAt ?? 0}|${snapshot?.updatedAt ?? 0}`; }, [running, lastTickAt, snapshot?.updatedAt, isChart]); const receiving = useLiveReceiveFlash(receiveSignal, running && !isChart); return (
{ if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onSelect(); } }} role={onSelect ? 'button' : undefined} tabIndex={onSelect ? 0 : undefined} aria-pressed={onSelect ? selected : undefined} >
{ko} {sym}
시간봉 {tf}
{onEnterFocus && ( )}
e.stopPropagation()}>
{!isChart && isDetail && evalCount > 0 && ( {metCount}/{evalCount} 조건 충족 )} {running && }
{isChart ? ( ) : ( )}
); }; export default VirtualTargetCard;