가상투자 메뉴 기능 구현

This commit is contained in:
Macbook
2026-05-25 06:41:45 +09:00
parent 0cfe7fc84c
commit 8b373b11e3
33 changed files with 3897 additions and 99 deletions
@@ -0,0 +1,101 @@
import React, { useMemo } from 'react';
import { getKoreanName } from '../../utils/marketNameCache';
import type { StrategyDto } from '../../utils/backendApi';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import {
buildConditionMetrics,
computeMatchRate,
formatUpdatedTime,
getTrafficLightState,
} from '../../utils/virtualSignalMetrics';
import VirtualLiveBadge from './VirtualLiveBadge';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import VirtualSignalEqualizer from './VirtualSignalEqualizer';
import VirtualSignalTrafficLight from './VirtualSignalTrafficLight';
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
import VirtualIndicatorCompareTable from './VirtualIndicatorCompareTable';
interface Props {
market: string;
strategy: StrategyDto | undefined;
snapshot: VirtualIndicatorSnapshot | undefined;
running: boolean;
liveStatus?: VirtualLiveStatus;
viewMode?: VirtualCardViewMode;
onOpenChart?: () => void;
}
const VirtualTargetCard: React.FC<Props> = ({
market, strategy, snapshot, running, liveStatus = 'idle', viewMode = 'summary', onOpenChart,
}) => {
const ko = getKoreanName(market);
const sym = market.replace(/^KRW-/, '');
const tf = snapshot?.timeframe ?? '—';
const rows = snapshot?.rows ?? [];
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
const matchRate = useMemo(
() => computeMatchRate(metrics, snapshot?.matchRate),
[metrics, snapshot?.matchRate],
);
const trafficState = useMemo(() => getTrafficLightState(matchRate, metrics), [matchRate, metrics]);
const metCount = metrics.filter(m => m.status === 'match').length;
const evalCount = metrics.filter(m => m.status !== 'unknown').length;
const isDetail = viewMode === 'detail';
return (
<div className={`vtd-card vtd-card--signal${running ? ' vtd-card--live' : ''}${isDetail ? ' vtd-card--detail' : ' vtd-card--summary'}`}>
<div className="vtd-card-head">
<div className="vtd-card-title">
<span className="vtd-card-ko">{ko}</span>
<span className="vtd-card-sym">{sym}</span>
</div>
<div className="vtd-card-head-actions">
<button
type="button"
className="vtd-card-chart-btn"
title="차트 보기"
aria-label="차트 보기"
onClick={onOpenChart}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
<rect x="3" y="4" width="18" height="14" rx="2"/>
<path d="M7 14l3-3 3 2 4-5"/>
</svg>
</button>
<span className="vtd-card-strat">{strategy?.name ?? '전략 미지정'}</span>
</div>
</div>
<div className="vtd-card-meta">
<span className="vtd-card-tf"> {tf}</span>
{isDetail && evalCount > 0 && (
<span className="vtd-card-met-count">{metCount}/{evalCount} </span>
)}
{running && <VirtualLiveBadge status={liveStatus} />}
</div>
{rows.length === 0 ? (
<p className="vtd-muted vtd-card-empty"> · </p>
) : (
<>
<div className={`vtd-sig-panel${isDetail ? '' : ' vtd-sig-panel--summary'}`}>
<div className="vtd-sig-panel-title">SIGNAL INTELLIGENCE &amp; MATCH RATES</div>
<div className="vtd-sig-visual">
<VirtualSignalEqualizer matchRate={matchRate} />
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
</div>
{isDetail && <VirtualIndicatorCompareTable metrics={metrics} />}
</div>
<div className="vtd-card-foot">
<span className="vtd-card-updated"> {formatUpdatedTime(snapshot?.updatedAt)}</span>
<span className="vtd-card-match-summary"> {matchRate}%</span>
</div>
</>
)}
</div>
);
};
export default VirtualTargetCard;