가상투자 화면 추가
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user