가상투자 설정 수정

This commit is contained in:
Macbook
2026-05-25 22:30:54 +09:00
parent cae1c3a624
commit 1e950c7db4
24 changed files with 1015 additions and 362 deletions
@@ -3,7 +3,7 @@ import type { StrategyDto } from '../../utils/backendApi';
import type { VirtualSessionConfig, VirtualCardViewMode } from '../../utils/virtualTradingStorage';
import type { VirtualCardDisplayMode } from './VirtualTargetCard';
interface Props {
interface SessionProps {
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
strategies: StrategyDto[];
onStrategyChange: (strategyId: number | null) => void;
@@ -11,15 +11,21 @@ interface Props {
onPositionModeChange: (mode: VirtualSessionConfig['positionMode']) => void;
onStart: () => void;
onStop: () => void;
viewMode?: VirtualCardViewMode;
onViewModeChange?: (mode: VirtualCardViewMode) => void;
globalDisplayMode?: VirtualCardDisplayMode;
onGlobalDisplayModeChange?: (mode: VirtualCardDisplayMode) => void;
/** 우측 뷰 토글: full=신호·차트+요약·상세, display-only=요약·상세만, hidden=숨김 */
viewControls?: 'full' | 'display-only' | 'hidden';
paperAutoTradeEnabled?: boolean;
onPaperAutoTradeEnabled?: (enabled: boolean) => void;
paperTradingEnabled?: boolean;
}
const VirtualGridUnifiedHeader: React.FC<Props> = ({
interface ViewProps {
session: Pick<VirtualSessionConfig, 'running'>;
viewMode: VirtualCardViewMode;
onViewModeChange: (mode: VirtualCardViewMode) => void;
globalDisplayMode?: VirtualCardDisplayMode;
onGlobalDisplayModeChange?: (mode: VirtualCardDisplayMode) => void;
showSignalChartToggle?: boolean;
}
export const VirtualSessionHeaderControls: React.FC<SessionProps> = ({
session,
strategies,
onStrategyChange,
@@ -27,155 +33,187 @@ const VirtualGridUnifiedHeader: React.FC<Props> = ({
onPositionModeChange,
onStart,
onStop,
viewMode = 'summary',
onViewModeChange,
globalDisplayMode = 'signal',
onGlobalDisplayModeChange,
viewControls = 'full',
paperAutoTradeEnabled = false,
onPaperAutoTradeEnabled,
paperTradingEnabled = true,
}) => (
<div className="vtd-unified-head">
<span className="vtd-unified-head-title"> </span>
<div className="vtd-header-session">
<div className="vtd-session-field">
<span className="vtd-session-label"></span>
<select
className="vtd-session-select"
value={session.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>
</div>
<div className="vtd-unified-head-center">
<div className="vtd-session-field">
<span className="vtd-session-label"></span>
<select
className="vtd-session-select"
value={session.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>
</div>
<div className="vtd-session-field">
<span className="vtd-session-label"></span>
<div className="vtd-view-toggle" role="group" aria-label="실행방식">
<button
type="button"
className={`vtd-view-toggle-btn${session.executionType === 'CANDLE_CLOSE' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onExecutionTypeChange('CANDLE_CLOSE')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${session.executionType === 'REALTIME_TICK' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onExecutionTypeChange('REALTIME_TICK')}
>
</button>
</div>
</div>
<div className="vtd-session-field">
<span className="vtd-session-label"> </span>
<div className="vtd-view-toggle" role="group" aria-label="시그널 모드">
<button
type="button"
className={`vtd-view-toggle-btn${session.positionMode === 'LONG_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onPositionModeChange('LONG_ONLY')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${session.positionMode === 'SIGNAL_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onPositionModeChange('SIGNAL_ONLY')}
>
</button>
</div>
</div>
<div className="vtd-session-actions">
<div className="vtd-session-field">
<span className="vtd-session-label"></span>
<div className="vtd-view-toggle" role="group" aria-label="실행방식">
<button
type="button"
className="bps-btn vtd-session-btn vtd-session-btn--start"
disabled={session.running}
onClick={onStart}
className={`vtd-view-toggle-btn${session.executionType === 'CANDLE_CLOSE' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onExecutionTypeChange('CANDLE_CLOSE')}
>
</button>
<button
type="button"
className="bps-btn bps-btn--danger vtd-session-btn"
disabled={!session.running}
onClick={onStop}
className={`vtd-view-toggle-btn${session.executionType === 'REALTIME_TICK' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onExecutionTypeChange('REALTIME_TICK')}
>
</button>
</div>
</div>
{viewControls !== 'hidden' && onViewModeChange && (
<div className="vtd-unified-head-right">
{viewControls === 'full' && onGlobalDisplayModeChange && (
<>
<div className="vtd-grid-head-group" role="group" aria-label="전체 종목 신호·차트">
<div className="vtd-view-toggle">
<button
type="button"
className={`vtd-view-toggle-btn${globalDisplayMode === 'signal' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onGlobalDisplayModeChange('signal')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${globalDisplayMode === 'chart' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onGlobalDisplayModeChange('chart')}
>
</button>
</div>
</div>
<span className="vtd-grid-head-divider" aria-hidden="true" />
</>
)}
<div className="vtd-grid-head-group" role="group" aria-label="카드 표시 방식">
<div className="vtd-view-toggle">
<button
type="button"
className={`vtd-view-toggle-btn${viewMode === 'summary' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onViewModeChange('summary')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${viewMode === 'detail' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onViewModeChange('detail')}
>
</button>
</div>
</div>
{viewMode === 'detail' && (
<>
<span className="vtd-grid-head-divider" aria-hidden="true" />
<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>
</>
)}
{session.running && (
<>
<span className="vtd-grid-head-divider" aria-hidden="true" />
<span className="vtd-grid-live"> 3</span>
</>
)}
<div className="vtd-session-field">
<span className="vtd-session-label"> </span>
<div className="vtd-view-toggle" role="group" aria-label="시그널 모드">
<button
type="button"
className={`vtd-view-toggle-btn${session.positionMode === 'LONG_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onPositionModeChange('LONG_ONLY')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${session.positionMode === 'SIGNAL_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onPositionModeChange('SIGNAL_ONLY')}
>
</button>
</div>
)}
</div>
<div className="vtd-session-field">
<span className="vtd-session-label"></span>
<div
className="vtd-view-toggle"
role="group"
aria-label="자동매매"
title={
!paperTradingEnabled
? '설정 › 가상투자에서 가상투자를 활성화하세요'
: paperAutoTradeEnabled
? 'Match 충족 시 자동 매수·매도'
: '수동 주문만 가능'
}
>
<button
type="button"
className={`vtd-view-toggle-btn${paperAutoTradeEnabled ? ' vtd-view-toggle-btn--on' : ''}`}
disabled={!paperTradingEnabled || !onPaperAutoTradeEnabled}
onClick={() => onPaperAutoTradeEnabled?.(true)}
>
ON
</button>
<button
type="button"
className={`vtd-view-toggle-btn${!paperAutoTradeEnabled ? ' vtd-view-toggle-btn--on' : ''}`}
disabled={!paperTradingEnabled || !onPaperAutoTradeEnabled}
onClick={() => onPaperAutoTradeEnabled?.(false)}
>
OFF
</button>
</div>
</div>
<div className="vtd-session-actions">
<button
type="button"
className="bps-btn vtd-session-btn vtd-session-btn--start"
disabled={session.running}
onClick={onStart}
>
</button>
<button
type="button"
className="bps-btn bps-btn--danger vtd-session-btn"
disabled={!session.running}
onClick={onStop}
>
</button>
</div>
</div>
);
export default VirtualGridUnifiedHeader;
export const VirtualViewHeaderControls: React.FC<ViewProps> = ({
session,
viewMode,
onViewModeChange,
globalDisplayMode = 'signal',
onGlobalDisplayModeChange,
showSignalChartToggle = true,
}) => (
<div className="vtd-header-view">
{showSignalChartToggle && onGlobalDisplayModeChange && (
<>
<div className="vtd-grid-head-group" role="group" aria-label="전체 종목 신호·차트">
<div className="vtd-view-toggle">
<button
type="button"
className={`vtd-view-toggle-btn${globalDisplayMode === 'signal' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onGlobalDisplayModeChange('signal')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${globalDisplayMode === 'chart' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onGlobalDisplayModeChange('chart')}
>
</button>
</div>
</div>
<span className="vtd-grid-head-divider" aria-hidden="true" />
</>
)}
<div className="vtd-grid-head-group" role="group" aria-label="카드 표시 방식">
<div className="vtd-view-toggle">
<button
type="button"
className={`vtd-view-toggle-btn${viewMode === 'summary' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onViewModeChange('summary')}
>
</button>
<button
type="button"
className={`vtd-view-toggle-btn${viewMode === 'detail' ? ' vtd-view-toggle-btn--on' : ''}`}
onClick={() => onViewModeChange('detail')}
>
</button>
</div>
</div>
{viewMode === 'detail' && (
<>
<span className="vtd-grid-head-divider" aria-hidden="true" />
<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>
</>
)}
{session.running && (
<>
<span className="vtd-grid-head-divider" aria-hidden="true" />
<span className="vtd-grid-live"> 3</span>
</>
)}
</div>
);
@@ -1,12 +1,10 @@
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 { 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';
import type { TickerData } from '../../hooks/useMarketTicker';
@@ -16,11 +14,6 @@ interface Props {
strategies: StrategyDto[];
snapshots: Record<string, VirtualIndicatorSnapshot>;
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>;
@@ -30,11 +23,13 @@ interface Props {
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
tickers?: Map<string, TickerData>;
viewMode: VirtualCardViewMode;
globalDisplayMode: VirtualCardDisplayMode;
onFocusMarketChange?: (market: string | null) => void;
}
const VirtualTargetGrid: React.FC<Props> = ({
targets, strategies, snapshots, session,
onStrategyChange, onExecutionTypeChange, onPositionModeChange, onStart, onStop,
onTargetStrategyChange,
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
onSelectMarket,
@@ -42,9 +37,10 @@ const VirtualTargetGrid: React.FC<Props> = ({
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
tickers,
viewMode,
globalDisplayMode,
onFocusMarketChange,
}) => {
const [viewMode, setViewMode] = useState<VirtualCardViewMode>(() => loadVirtualCardViewMode());
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
const [focusMarket, setFocusMarket] = useState<string | null>(null);
@@ -53,14 +49,14 @@ const VirtualTargetGrid: React.FC<Props> = ({
useEffect(() => {
if (focusMarket && !activeMarkets.has(focusMarket)) {
setFocusMarket(null);
onFocusMarketChange?.(null);
}
}, [activeMarkets, focusMarket]);
}, [activeMarkets, focusMarket, onFocusMarketChange]);
useEffect(() => {
saveVirtualCardViewMode(viewMode);
}, [viewMode]);
onFocusMarketChange?.(focusMarket);
}, [focusMarket, onFocusMarketChange]);
/** 제거된 종목 override 정리 */
useEffect(() => {
setCardDisplayOverrides(prev => {
const next: Record<string, VirtualCardDisplayMode> = {};
@@ -73,10 +69,9 @@ const VirtualTargetGrid: React.FC<Props> = ({
});
}, [activeMarkets]);
const handleGlobalDisplayMode = useCallback((mode: VirtualCardDisplayMode) => {
setGlobalDisplayMode(mode);
useEffect(() => {
setCardDisplayOverrides({});
}, []);
}, [globalDisplayMode]);
const handleCardDisplayMode = useCallback((market: string, mode: VirtualCardDisplayMode) => {
setCardDisplayOverrides(prev => ({ ...prev, [market]: mode }));
@@ -92,27 +87,9 @@ const VirtualTargetGrid: React.FC<Props> = ({
[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-wrap">
{renderUnifiedHeader('hidden')}
<div className="vtd-grid-empty">
<p className="vtd-muted"> .</p>
</div>
@@ -122,7 +99,6 @@ const VirtualTargetGrid: React.FC<Props> = ({
return (
<div className={`vtd-grid-wrap${focusMarket ? ' vtd-grid-wrap--focus' : ''}`}>
{renderUnifiedHeader(focusMarket ? 'display-only' : 'full')}
{focusMarket && focusTarget ? (
<VirtualTargetFocusView
market={focusTarget.market}