가상투자 화면 추가

This commit is contained in:
Macbook
2026-05-25 13:14:36 +09:00
parent 1fe3e404fa
commit 339e2355eb
23 changed files with 2230 additions and 457 deletions
@@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react';
import VirtualTargetCard from './VirtualTargetCard';
import VirtualStrategyChartPopup from './VirtualStrategyChartPopup';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import VirtualTargetCard, { type VirtualCardDisplayMode } from './VirtualTargetCard';
import VirtualGridUnifiedHeader from './VirtualGridUnifiedHeader';
import VirtualTargetFocusView from './VirtualTargetFocusView';
import type { StrategyDto } from '../../utils/backendApi';
import type { Theme } from '../../types';
import type { VirtualTargetItem, VirtualCardViewMode } from '../../utils/virtualTradingStorage';
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
import type { VirtualTargetItem, VirtualCardViewMode, VirtualSessionConfig } from '../../utils/virtualTradingStorage';
import { loadVirtualCardViewMode, saveVirtualCardViewMode } from '../../utils/virtualTradingStorage';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
@@ -12,90 +14,156 @@ interface Props {
targets: VirtualTargetItem[];
strategies: StrategyDto[];
snapshots: Record<string, VirtualIndicatorSnapshot>;
running: boolean;
globalStrategyId: number | null;
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
onStrategyChange: (strategyId: number | null) => void;
onExecutionTypeChange: (type: VirtualSessionConfig['executionType']) => void;
onPositionModeChange: (mode: VirtualSessionConfig['positionMode']) => void;
onStart: () => void;
onStop: () => void;
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
lastTickAtByMarket?: Record<string, number>;
theme?: Theme;
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
}
const VirtualTargetGrid: React.FC<Props> = ({
targets, strategies, snapshots, running, globalStrategyId, liveStatusByMarket = {}, theme = 'dark',
targets, strategies, snapshots, session,
onStrategyChange, onExecutionTypeChange, onPositionModeChange, onStart, onStop,
onTargetStrategyChange,
liveStatusByMarket = {}, lastTickAtByMarket = {}, theme = 'dark',
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
}) => {
const [viewMode, setViewMode] = useState<VirtualCardViewMode>(() => loadVirtualCardViewMode());
const [chartTarget, setChartTarget] = useState<{ market: string; strategy: StrategyDto | undefined } | null>(null);
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
const [focusMarket, setFocusMarket] = useState<string | null>(null);
const activeMarkets = useMemo(() => new Set(targets.map(t => t.market)), [targets]);
useEffect(() => {
if (focusMarket && !activeMarkets.has(focusMarket)) {
setFocusMarket(null);
}
}, [activeMarkets, focusMarket]);
useEffect(() => {
saveVirtualCardViewMode(viewMode);
}, [viewMode]);
/** 제거된 종목 override 정리 */
useEffect(() => {
setCardDisplayOverrides(prev => {
const next: Record<string, VirtualCardDisplayMode> = {};
let changed = false;
for (const [market, mode] of Object.entries(prev)) {
if (activeMarkets.has(market)) next[market] = mode;
else changed = true;
}
return changed ? next : prev;
});
}, [activeMarkets]);
const handleGlobalDisplayMode = useCallback((mode: VirtualCardDisplayMode) => {
setGlobalDisplayMode(mode);
setCardDisplayOverrides({});
}, []);
const handleCardDisplayMode = useCallback((market: string, mode: VirtualCardDisplayMode) => {
setCardDisplayOverrides(prev => ({ ...prev, [market]: mode }));
}, []);
const resolveCardDisplayMode = useCallback(
(market: string) => cardDisplayOverrides[market] ?? globalDisplayMode,
[cardDisplayOverrides, globalDisplayMode],
);
const focusTarget = useMemo(
() => (focusMarket ? targets.find(t => t.market === focusMarket) : undefined),
[focusMarket, targets],
);
const renderUnifiedHeader = (viewControls: 'full' | 'display-only' | 'hidden') => (
<VirtualGridUnifiedHeader
session={session}
strategies={strategies}
onStrategyChange={onStrategyChange}
onExecutionTypeChange={onExecutionTypeChange}
onPositionModeChange={onPositionModeChange}
onStart={onStart}
onStop={onStop}
viewMode={viewMode}
onViewModeChange={setViewMode}
globalDisplayMode={globalDisplayMode}
onGlobalDisplayModeChange={handleGlobalDisplayMode}
viewControls={viewControls}
/>
);
if (targets.length === 0) {
return (
<div className="vtd-grid-empty">
<p className="vtd-muted"> .</p>
<div className="vtd-grid-wrap">
{renderUnifiedHeader('hidden')}
<div className="vtd-grid-empty">
<p className="vtd-muted"> .</p>
</div>
</div>
);
}
return (
<>
<div className="vtd-grid-wrap">
<div className="vtd-grid-head">
<span className="vtd-grid-head-title"> </span>
<div className="vtd-grid-head-right">
<div className="vtd-view-toggle" role="group" aria-label="카드 표시 방식">
<button
type="button"
className={`vtd-view-toggle-btn${viewMode === 'summary' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => setViewMode('summary')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${viewMode === 'detail' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => setViewMode('detail')}
>
</button>
</div>
{viewMode === 'detail' && (
<div className="vtd-grid-head-legend">
<span className="vtd-legend vtd-legend--match"> </span>
<span className="vtd-legend vtd-legend--pending"> </span>
<span className="vtd-legend vtd-legend--nomatch"> </span>
</div>
)}
{running && <span className="vtd-grid-live"> 3</span>}
</div>
</div>
<div className={`vtd-grid-wrap${focusMarket ? ' vtd-grid-wrap--focus' : ''}`}>
{renderUnifiedHeader(focusMarket ? 'display-only' : 'full')}
{focusMarket && focusTarget ? (
<VirtualTargetFocusView
market={focusTarget.market}
strategy={strategies.find(s => s.id === (focusTarget.strategyId ?? session.globalStrategyId))}
strategies={strategies}
strategyId={focusTarget.strategyId}
globalStrategyId={session.globalStrategyId}
onStrategyChange={id => onTargetStrategyChange(focusTarget.market, id)}
snapshot={snapshots[focusTarget.market]}
running={session.running}
liveStatus={liveStatusByMarket[focusTarget.market] ?? (session.running ? 'connecting' : 'idle')}
lastTickAt={lastTickAtByMarket[focusTarget.market]}
viewMode={viewMode}
onExit={() => setFocusMarket(null)}
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
/>
) : (
<div className="vtd-grid">
{targets.map(t => {
const strat = strategies.find(s => s.id === (t.strategyId ?? globalStrategyId));
const strat = strategies.find(s => s.id === (t.strategyId ?? session.globalStrategyId));
return (
<VirtualTargetCard
key={t.market}
market={t.market}
strategy={strat}
strategies={strategies}
strategyId={t.strategyId}
globalStrategyId={session.globalStrategyId}
onStrategyChange={id => onTargetStrategyChange(t.market, id)}
snapshot={snapshots[t.market]}
running={running}
liveStatus={liveStatusByMarket[t.market] ?? (running ? 'connecting' : 'idle')}
running={session.running}
liveStatus={liveStatusByMarket[t.market] ?? (session.running ? 'connecting' : 'idle')}
lastTickAt={lastTickAtByMarket[t.market]}
viewMode={viewMode}
onOpenChart={() => setChartTarget({ market: t.market, strategy: strat })}
displayMode={resolveCardDisplayMode(t.market)}
onDisplayModeChange={mode => handleCardDisplayMode(t.market, mode)}
onEnterFocus={() => setFocusMarket(t.market)}
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
/>
);
})}
</div>
</div>
{chartTarget && (
<VirtualStrategyChartPopup
market={chartTarget.market}
strategy={chartTarget.strategy}
theme={theme}
running={running}
onClose={() => setChartTarget(null)}
/>
)}
</>
</div>
);
};