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 } from '../../utils/virtualSignalMetrics'; import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators'; import type { Timeframe } from '../../types'; 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'; import VirtualTargetCardFoot from './VirtualTargetCardFoot'; export type VirtualCardDisplayMode = 'signal' | 'chart'; /** default: 우측 액션 버튼 · inline-quote: 우측 현재가·등락·실시간 배지 */ export type VirtualCardHeadVariant = 'default' | 'inline-quote'; interface Props { market: string; strategy: StrategyDto | undefined; strategies: StrategyDto[]; strategyId: number | null; globalStrategyId: number | null; onStrategyChange?: (strategyId: number | null) => void; snapshot: VirtualIndicatorSnapshot | undefined; signalLoading?: boolean; running: boolean; liveStatus?: VirtualLiveStatus; lastTickAt?: number; viewMode?: VirtualCardViewMode; displayMode?: VirtualCardDisplayMode; onDisplayModeChange?: (mode: VirtualCardDisplayMode) => void; onEnterFocus?: () => void; selected?: boolean; onSelect?: () => void; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; chartLiveReceiveHighlight?: boolean; ticker?: TickerData; /** 알림 목록 등 — 푸터 전략 셀렉트 읽기 전용 라벨 */ readOnlyStrategyLabel?: string; /** 헤더 우측 레이아웃 (알림 목록 신호 슬롯: inline-quote) */ headVariant?: VirtualCardHeadVariant; } const VirtualTargetCard: React.FC = ({ market, strategy, strategies, strategyId, globalStrategyId, onStrategyChange, snapshot, signalLoading = false, running, liveStatus = 'idle', lastTickAt, viewMode = 'summary', displayMode = 'signal', onDisplayModeChange, onEnterFocus, selected = false, onSelect, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, chartLiveReceiveHighlight = true, ticker, readOnlyStrategyLabel, headVariant = 'default', }) => { const inlineHeadQuote = headVariant === 'inline-quote'; const ko = getKoreanName(market); const sym = market.replace(/^KRW-/, ''); const rows = snapshot?.rows ?? []; const metrics = useMemo(() => buildConditionMetrics(rows), [rows]); 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 chartTimeframe = useMemo( () => resolveStrategyPrimaryTimeframe(strategy) as Timeframe, [strategy], ); const receiveSignal = useMemo(() => { if (!running) return null; return `${lastTickAt ?? 0}|${snapshot?.updatedAt ?? 0}`; }, [running, lastTickAt, snapshot?.updatedAt]); const receiving = useLiveReceiveFlash(receiveSignal, running); const highlightReceiving = chartLiveReceiveHighlight && receiving; 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}
{inlineHeadQuote ? (
e.stopPropagation()}> {running && }
) : (
{onEnterFocus && ( )}
e.stopPropagation()}>
)}
{!inlineHeadQuote && (
{!isChart && isDetail && evalCount > 0 && ( {metCount}/{evalCount} 조건 충족 )} {running && }
)} {isChart ? ( ) : ( )}
); }; export default VirtualTargetCard;