가상투자 종목카드박스 레이아웃 수정

This commit is contained in:
Macbook
2026-05-26 16:42:57 +09:00
parent 7cf43609dc
commit c0d21ac825
25 changed files with 1632 additions and 61 deletions
@@ -5,6 +5,9 @@ import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSn
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
import { buildConditionMetrics, resolveVirtualTradeTiming } from '../../utils/virtualSignalMetrics';
import { candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
import { normalizeStartCandleType } from '../../utils/strategyStartNodes';
import type { Timeframe } from '../../types';
import VirtualLiveBadge from './VirtualLiveBadge';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
@@ -13,6 +16,7 @@ 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';
@@ -23,6 +27,8 @@ interface Props {
strategyId: number | null;
globalStrategyId: number | null;
onStrategyChange?: (strategyId: number | null) => void;
candleType: string;
onCandleTypeChange?: (candleType: string) => void;
snapshot: VirtualIndicatorSnapshot | undefined;
running: boolean;
liveStatus?: VirtualLiveStatus;
@@ -47,6 +53,8 @@ const VirtualTargetCard: React.FC<Props> = ({
strategyId,
globalStrategyId,
onStrategyChange,
candleType,
onCandleTypeChange,
snapshot,
running,
liveStatus = 'idle',
@@ -65,7 +73,6 @@ const VirtualTargetCard: React.FC<Props> = ({
}) => {
const ko = getKoreanName(market);
const sym = market.replace(/^KRW-/, '');
const tf = snapshot?.timeframe ?? '—';
const rows = snapshot?.rows ?? [];
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
@@ -75,6 +82,8 @@ const VirtualTargetCard: React.FC<Props> = ({
const isDetail = viewMode === 'detail';
const isChart = displayMode === 'chart';
const evalCandleType = normalizeStartCandleType(candleType);
const chartTimeframe = candleTypeToTimeframe(evalCandleType) as Timeframe;
const receiveSignal = useMemo(() => {
if (!running) return null;
@@ -113,23 +122,6 @@ const VirtualTargetCard: React.FC<Props> = ({
<span className="vtd-card-ko">{ko}</span>
<span className="vtd-card-sym">{sym}</span>
</div>
<label className="vtd-card-strategy-field" onClick={e => e.stopPropagation()}>
<select
className="vtd-card-strategy-select"
aria-label="투자전략"
value={strategyId ?? globalStrategyId ?? ''}
onChange={e => {
const v = e.target.value;
onStrategyChange?.(v ? Number(v) : null);
}}
>
<option value=""> </option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
<span className="vtd-card-tf"> {tf}</span>
</div>
<div className="vtd-card-head-actions">
{onEnterFocus && (
@@ -185,6 +177,7 @@ const VirtualTargetCard: React.FC<Props> = ({
running={running}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
chartTimeframe={chartTimeframe}
/>
) : (
<VirtualTargetSignalPanel
@@ -193,6 +186,16 @@ const VirtualTargetCard: React.FC<Props> = ({
receiving={highlightReceiving}
/>
)}
<VirtualTargetCardFoot
snapshot={snapshot}
strategies={strategies}
strategyId={strategyId}
globalStrategyId={globalStrategyId}
candleType={evalCandleType}
onStrategyChange={onStrategyChange}
onCandleTypeChange={onCandleTypeChange}
/>
</div>
);
};
@@ -33,6 +33,8 @@ interface Props {
chartSeriesPriceLabels?: boolean;
/** 전체보기 분할 pane — 캔버스 높이 100% */
fillHeight?: boolean;
/** 카드 푸터 평가 분봉과 동기화 */
chartTimeframe?: Timeframe;
}
const noop = () => {};
@@ -45,14 +47,21 @@ const VirtualTargetCardChart: React.FC<Props> = ({
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
fillHeight = false,
chartTimeframe,
}) => {
const { getParams, getVisualConfig } = useIndicatorSettings();
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
const [timeframe, setTimeframe] = useState<Timeframe>(() => resolveStrategyPrimaryTimeframe(strategy));
const [timeframe, setTimeframe] = useState<Timeframe>(
() => chartTimeframe ?? resolveStrategyPrimaryTimeframe(strategy),
);
useEffect(() => {
if (chartTimeframe) {
setTimeframe(chartTimeframe);
return;
}
setTimeframe(resolveStrategyPrimaryTimeframe(strategy));
}, [strategy, market]);
}, [strategy, market, chartTimeframe]);
const indicators = useMemo(
() => buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig),
@@ -0,0 +1,62 @@
import React from 'react';
import type { StrategyDto } from '../../utils/backendApi';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import { formatUpdatedTime } from '../../utils/virtualSignalMetrics';
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../../utils/strategyStartNodes';
interface Props {
snapshot: VirtualIndicatorSnapshot | undefined;
strategies: StrategyDto[];
strategyId: number | null;
globalStrategyId: number | null;
candleType: string;
onStrategyChange?: (strategyId: number | null) => void;
onCandleTypeChange?: (candleType: string) => void;
}
/** 카드 하단 — 갱신 시각(좌) · 전략·시간봉 드롭다운(우) */
const VirtualTargetCardFoot: React.FC<Props> = ({
snapshot,
strategies,
strategyId,
globalStrategyId,
candleType,
onStrategyChange,
onCandleTypeChange,
}) => (
<div className="vtd-card-foot">
<span className="vtd-card-updated"> {formatUpdatedTime(snapshot?.updatedAt)}</span>
<div className="vtd-card-foot-controls" onClick={e => e.stopPropagation()}>
<label className="vtd-card-foot-field">
<select
className="vtd-card-foot-select"
aria-label="투자전략"
value={strategyId ?? globalStrategyId ?? ''}
onChange={e => {
const v = e.target.value;
onStrategyChange?.(v ? Number(v) : null);
}}
>
<option value=""> </option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
<label className="vtd-card-foot-field">
<select
className="vtd-card-foot-select vtd-card-foot-select--tf"
aria-label="시간봉"
value={candleType}
onChange={e => onCandleTypeChange?.(e.target.value)}
>
{STRATEGY_CANDLE_TYPE_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</label>
</div>
</div>
);
export default VirtualTargetCardFoot;
@@ -12,6 +12,10 @@ import VirtualLiveBadge from './VirtualLiveBadge';
import VirtualTargetQuote from './VirtualTargetQuote';
import VirtualTargetCardChart from './VirtualTargetCardChart';
import VirtualTargetSignalPanel from './VirtualTargetSignalPanel';
import VirtualTargetCardFoot from './VirtualTargetCardFoot';
import { candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
import { normalizeStartCandleType } from '../../utils/strategyStartNodes';
import type { Timeframe } from '../../types';
interface Props {
market: string;
@@ -20,6 +24,8 @@ interface Props {
strategyId: number | null;
globalStrategyId: number | null;
onStrategyChange?: (strategyId: number | null) => void;
candleType: string;
onCandleTypeChange?: (candleType: string) => void;
snapshot: VirtualIndicatorSnapshot | undefined;
running: boolean;
liveStatus?: VirtualLiveStatus;
@@ -41,6 +47,8 @@ const VirtualTargetFocusView: React.FC<Props> = ({
strategyId,
globalStrategyId,
onStrategyChange,
candleType,
onCandleTypeChange,
snapshot,
running,
liveStatus = 'idle',
@@ -55,7 +63,6 @@ const VirtualTargetFocusView: React.FC<Props> = ({
}) => {
const ko = getKoreanName(market);
const sym = market.replace(/^KRW-/, '');
const tf = snapshot?.timeframe ?? '—';
const receiveSignal = useMemo(() => {
if (!running) return null;
@@ -63,6 +70,8 @@ const VirtualTargetFocusView: React.FC<Props> = ({
}, [running, lastTickAt, snapshot?.updatedAt]);
const receiving = useLiveReceiveFlash(receiveSignal, running);
const highlightReceiving = chartLiveReceiveHighlight && receiving;
const evalCandleType = normalizeStartCandleType(candleType);
const chartTimeframe = candleTypeToTimeframe(evalCandleType) as Timeframe;
return (
<div className={`vtd-focus-wrap${highlightReceiving ? ' vtd-focus-wrap--receiving' : ''}`}>
@@ -72,23 +81,6 @@ const VirtualTargetFocusView: React.FC<Props> = ({
<span className="vtd-card-ko">{ko}</span>
<span className="vtd-card-sym">{sym}</span>
</div>
<label className="vtd-card-strategy-field" onClick={e => e.stopPropagation()}>
<select
className="vtd-card-strategy-select"
aria-label="투자전략"
value={strategyId ?? globalStrategyId ?? ''}
onChange={e => {
const v = e.target.value;
onStrategyChange?.(v ? Number(v) : null);
}}
>
<option value=""> </option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
<span className="vtd-card-tf"> {tf}</span>
{running && <VirtualLiveBadge status={liveStatus} receiving={receiving} />}
</div>
<button
@@ -123,6 +115,7 @@ const VirtualTargetFocusView: React.FC<Props> = ({
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
fillHeight
chartTimeframe={chartTimeframe}
/>
</div>
</section>
@@ -137,6 +130,16 @@ const VirtualTargetFocusView: React.FC<Props> = ({
</div>
</section>
</div>
<VirtualTargetCardFoot
snapshot={snapshot}
strategies={strategies}
strategyId={strategyId}
globalStrategyId={globalStrategyId}
candleType={evalCandleType}
onStrategyChange={onStrategyChange}
onCandleTypeChange={onCandleTypeChange}
/>
</div>
);
};
@@ -4,7 +4,12 @@ import VirtualTargetFocusView from './VirtualTargetFocusView';
import type { StrategyDto } from '../../utils/backendApi';
import type { Theme } from '../../types';
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
import type { VirtualTargetItem, VirtualCardViewMode, VirtualSessionConfig } from '../../utils/virtualTradingStorage';
import {
type VirtualTargetItem,
type VirtualCardViewMode,
type VirtualSessionConfig,
resolveTargetCandleType,
} from '../../utils/virtualTradingStorage';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import type { TickerData } from '../../hooks/useMarketTicker';
@@ -15,6 +20,7 @@ interface Props {
snapshots: Record<string, VirtualIndicatorSnapshot>;
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
onTargetCandleTypeChange: (market: string, candleType: string) => void;
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
lastTickAtByMarket?: Record<string, number>;
selectedMarket?: string;
@@ -32,6 +38,7 @@ interface Props {
const VirtualTargetGrid: React.FC<Props> = ({
targets, strategies, snapshots, session,
onTargetStrategyChange,
onTargetCandleTypeChange,
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
onSelectMarket,
theme = 'dark',
@@ -109,6 +116,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
strategyId={focusTarget.strategyId}
globalStrategyId={session.globalStrategyId}
onStrategyChange={id => onTargetStrategyChange(focusTarget.market, id)}
candleType={resolveTargetCandleType(focusTarget, snapshots[focusTarget.market]?.timeframe)}
onCandleTypeChange={ct => onTargetCandleTypeChange(focusTarget.market, ct)}
snapshot={snapshots[focusTarget.market]}
running={session.running}
liveStatus={liveStatusByMarket[focusTarget.market] ?? (session.running ? 'connecting' : 'idle')}
@@ -134,6 +143,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
strategyId={t.strategyId}
globalStrategyId={session.globalStrategyId}
onStrategyChange={id => onTargetStrategyChange(t.market, id)}
candleType={resolveTargetCandleType(t, snapshots[t.market]?.timeframe)}
onCandleTypeChange={ct => onTargetCandleTypeChange(t.market, ct)}
snapshot={snapshots[t.market]}
running={session.running}
liveStatus={liveStatusByMarket[t.market] ?? (session.running ? 'connecting' : 'idle')}
@@ -3,7 +3,6 @@ import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSn
import {
buildConditionMetrics,
computeMatchRate,
formatUpdatedTime,
getTrafficLightState,
} from '../../utils/virtualSignalMetrics';
import VirtualSignalEqualizer from './VirtualSignalEqualizer';
@@ -67,10 +66,6 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
<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>
);
};