가상 수정

This commit is contained in:
Macbook
2026-05-25 16:02:52 +09:00
parent 3102169541
commit 182b82e990
18 changed files with 747 additions and 105 deletions
@@ -261,6 +261,7 @@ const VirtualTradingPage: React.FC<Props> = ({
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
tickers={tickers}
/>
)}
rightTabs={rightTabs}
@@ -3,24 +3,28 @@ import type { ConditionStatus } from '../../utils/virtualSignalMetrics';
interface Props {
status: ConditionStatus;
/** 조건 매칭율 0~100 */
matchRate?: number | null;
/** @deprecated matchRate 사용 */
progress?: number | null;
compact?: boolean;
}
const LABELS: Record<ConditionStatus, string> = {
match: 'Match',
pending: 'Pending',
nomatch: 'Mismatch',
match: '충족',
pending: '대기',
nomatch: '미충족',
unknown: '—',
};
const ConditionStatusSignal: React.FC<Props> = ({ status, progress, compact = false }) => {
const ConditionStatusSignal: React.FC<Props> = ({ status, matchRate, progress, compact = false }) => {
if (status === 'unknown') {
return <span className="vtd-cond-signal vtd-cond-signal--unknown"></span>;
}
const pct = progress != null && Number.isFinite(progress)
? `${Math.round(progress)}%`
const rate = matchRate ?? progress;
const pct = rate != null && Number.isFinite(rate)
? `${Math.round(rate)}%`
: null;
return (
@@ -1,26 +1,70 @@
import React from 'react';
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
import ConditionStatusSignal from './ConditionStatusSignal';
import { resolveHeatTier } from '../../utils/virtualSignalMetrics';
interface Props {
metrics: ConditionMetric[];
}
const IconMismatch = () => (
<svg className="vtd-heat-icon-svg" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden>
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
</svg>
);
const IconPending = () => (
<svg className="vtd-heat-icon-svg" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden>
<path d="M12 9v4" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<line x1="12" y1="17" x2="12.01" y2="17" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<path
d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
stroke="currentColor"
strokeWidth="1.8"
strokeLinejoin="round"
/>
</svg>
);
const IconMatch = () => (
<svg className="vtd-heat-icon-svg" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden>
<polyline points="20 6 9 17 4 12" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
const VirtualConditionList: React.FC<Props> = ({ metrics }) => {
if (metrics.length === 0) {
return <p className="vtd-cond-list-empty vtd-muted"> </p>;
return <p className="vtd-heat-list-empty vtd-muted"> </p>;
}
return (
<ul className="vtd-cond-list" aria-label="전략 조건 목록">
{metrics.map(({ row, status, progress }) => (
<li key={row.id} className="vtd-cond-list-item">
<span className="vtd-cond-list-name" title={row.displayName}>
{row.displayName}
</span>
<ConditionStatusSignal status={status} progress={progress} compact />
</li>
))}
<ul className="vtd-heat-list" aria-label="전략 지표 매치율">
{metrics.map(({ row, status, matchRate }) => {
const tier = resolveHeatTier(matchRate, status);
const pct = Math.min(100, Math.max(0, matchRate));
const minFill = pct > 0 && tier === 'nomatch' ? 6 : pct > 0 ? 6 : 0;
return (
<li key={row.id} className={`vtd-heat-row vtd-heat-row--${tier}`}>
<div
className="vtd-heat-track"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={pct}
aria-label={`${row.displayName} 매치율 ${pct}%`}
>
<span className="vtd-heat-label" title={row.displayName}>
{row.displayName}
</span>
<div className="vtd-heat-fill" style={{ width: `${Math.max(pct, minFill)}%` }} />
<span className="vtd-heat-pct">{pct}%</span>
</div>
<span className="vtd-heat-icon" aria-hidden>
{tier === 'match' ? <IconMatch /> : tier === 'pending' ? <IconPending /> : <IconMismatch />}
</span>
</li>
);
})}
</ul>
);
};
@@ -1,6 +1,6 @@
import React from 'react';
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
import { formatStrategyThreshold } from '../../utils/virtualSignalMetrics';
import { formatStrategyThreshold, resolveHeatTier } from '../../utils/virtualSignalMetrics';
import { formatIndicatorValue } from '../../utils/virtualStrategyConditions';
import ConditionStatusSignal from './ConditionStatusSignal';
@@ -10,19 +10,28 @@ interface Props {
const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
<div className="vtd-sig-table-wrap">
<div className="vtd-sig-table-head">REAL-TIME INDICATOR COMPARISON</div>
<div className="vtd-sig-table-head"> </div>
<table className="vtd-sig-table">
<colgroup>
<col className="vtd-sig-col vtd-sig-col--ind" />
<col className="vtd-sig-col vtd-sig-col--val" />
<col className="vtd-sig-col vtd-sig-col--threshold" />
<col className="vtd-sig-col vtd-sig-col--progress" />
<col className="vtd-sig-col vtd-sig-col--status" />
</colgroup>
<thead>
<tr>
<th>TECHNICAL INDICATOR</th>
<th>CURRENT VALUE</th>
<th>STRATEGY THRESHOLD</th>
<th>PROGRESS</th>
<th>STATUS</th>
<th className="vtd-sig-table-th--ind"> </th>
<th className="vtd-sig-table-th--val"></th>
<th className="vtd-sig-table-th--threshold"> </th>
<th className="vtd-sig-table-th--progress"></th>
<th className="vtd-sig-table-th--status"></th>
</tr>
</thead>
<tbody>
{metrics.map(({ row, status, progress }) => (
{metrics.map(({ row, status, matchRate }) => {
const tier = resolveHeatTier(matchRate, status);
return (
<tr key={row.id}>
<td className="vtd-sig-table-ind">{row.displayName}</td>
<td className="vtd-sig-table-val">{formatIndicatorValue(row.currentValue)}</td>
@@ -31,17 +40,20 @@ const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
?? formatStrategyThreshold(row.conditionType, row.targetValue, row.conditionLabel)}
</td>
<td className="vtd-sig-table-progress">
<div className="vtd-sig-row-bar">
<div className="vtd-sig-row-bar vtd-sig-row-bar--heat">
<div
className={`vtd-sig-row-bar-fill vtd-sig-row-bar-fill--${status}`}
style={{ width: `${progress ?? 0}%` }}
className={`vtd-sig-row-bar-fill vtd-sig-row-bar-fill--${tier}`}
style={{ width: `${matchRate}%` }}
/>
</div>
<span className="vtd-sig-row-pct">{progress != null ? `${Math.round(progress)}%` : '—'}</span>
<span className="vtd-sig-table-pct">{`${matchRate}%`}</span>
</td>
<td className="vtd-sig-table-status-cell">
<ConditionStatusSignal status={status} matchRate={matchRate} compact />
</td>
<td><ConditionStatusSignal status={status} progress={progress} /></td>
</tr>
))}
);
})}
</tbody>
</table>
</div>
@@ -11,6 +11,8 @@ import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
import VirtualTargetCardChart from './VirtualTargetCardChart';
import VirtualTargetSignalPanel from './VirtualTargetSignalPanel';
import type { Theme } from '../../types';
import type { TickerData } from '../../hooks/useMarketTicker';
import VirtualTargetQuote from './VirtualTargetQuote';
export type VirtualCardDisplayMode = 'signal' | 'chart';
@@ -34,6 +36,7 @@ interface Props {
theme?: Theme;
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
ticker?: TickerData;
}
const VirtualTargetCard: React.FC<Props> = ({
@@ -56,6 +59,7 @@ const VirtualTargetCard: React.FC<Props> = ({
theme = 'dark',
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
ticker,
}) => {
const ko = getKoreanName(market);
const sym = market.replace(/^KRW-/, '');
@@ -122,6 +126,7 @@ const VirtualTargetCard: React.FC<Props> = ({
))}
</select>
</label>
<span className="vtd-card-tf"> {tf}</span>
</div>
<div className="vtd-card-head-actions">
{onEnterFocus && (
@@ -162,7 +167,7 @@ const VirtualTargetCard: React.FC<Props> = ({
</div>
<div className="vtd-card-meta">
<span className="vtd-card-tf"> {tf}</span>
<VirtualTargetQuote market={market} ticker={ticker} compact />
{!isChart && isDetail && evalCount > 0 && (
<span className="vtd-card-met-count">{metCount}/{evalCount} </span>
)}
@@ -7,7 +7,9 @@ import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
import type { Theme } from '../../types';
import type { TickerData } from '../../hooks/useMarketTicker';
import VirtualLiveBadge from './VirtualLiveBadge';
import VirtualTargetQuote from './VirtualTargetQuote';
import VirtualTargetCardChart from './VirtualTargetCardChart';
import VirtualTargetSignalPanel from './VirtualTargetSignalPanel';
@@ -27,6 +29,7 @@ interface Props {
theme?: Theme;
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
ticker?: TickerData;
}
/** 전체보기 — 좌 차트 · 우 신호 */
@@ -46,6 +49,7 @@ const VirtualTargetFocusView: React.FC<Props> = ({
theme = 'dark',
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
ticker,
}) => {
const ko = getKoreanName(market);
const sym = market.replace(/^KRW-/, '');
@@ -100,6 +104,10 @@ const VirtualTargetFocusView: React.FC<Props> = ({
</button>
</div>
<div className="vtd-focus-meta">
<VirtualTargetQuote market={market} ticker={ticker} compact />
</div>
<div className="vtd-focus-split">
<section className="vtd-focus-pane vtd-focus-pane--chart" aria-label="차트 보기">
<div className="vtd-focus-pane-head"></div>
@@ -9,6 +9,7 @@ import type { VirtualTargetItem, VirtualCardViewMode, VirtualSessionConfig } fro
import { loadVirtualCardViewMode, saveVirtualCardViewMode } from '../../utils/virtualTradingStorage';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import type { TickerData } from '../../hooks/useMarketTicker';
interface Props {
targets: VirtualTargetItem[];
@@ -28,6 +29,7 @@ interface Props {
theme?: Theme;
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
tickers?: Map<string, TickerData>;
}
const VirtualTargetGrid: React.FC<Props> = ({
@@ -39,6 +41,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
theme = 'dark',
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
tickers,
}) => {
const [viewMode, setViewMode] = useState<VirtualCardViewMode>(() => loadVirtualCardViewMode());
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
@@ -137,6 +140,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
ticker={tickers?.get(focusTarget.market)}
/>
) : (
<div className="vtd-grid">
@@ -164,6 +168,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
ticker={tickers?.get(t.market)}
/>
);
})}
@@ -25,9 +25,11 @@ const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir })
interface Props {
market: string;
ticker?: TickerData;
/** 카드 메타 행 — 현재가만 간략 표시 */
compact?: boolean;
}
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker }) => {
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker, compact = false }) => {
const code = market.replace(/^KRW-/, '');
const change: ChangeDir = ticker?.change ?? 'EVEN';
const colorCls = change === 'RISE' ? 'vtd-target-up' : change === 'FALL' ? 'vtd-target-dn' : 'vtd-target-even';
@@ -49,6 +51,20 @@ const VirtualTargetQuote: React.FC<Props> = ({ market, ticker }) => {
prevPriceRef.current = price;
}, [ticker?.tradePrice]);
if (compact) {
return (
<div className={`vtd-target-quote vtd-target-quote--compact ${flashClass}`.trim()} aria-label="현재가">
<span className="vtd-card-price-label"></span>
<span className={`vtd-target-price ${colorCls}`}>
{ticker ? fmtKrw(ticker.tradePrice) : '-'}
</span>
<span className={`vtd-target-change ${colorCls}`}>
{ticker ? fmtPct(ticker.changeRate) : '-'}
</span>
</div>
);
}
return (
<div className={`vtd-target-quote ${flashClass}`.trim()} aria-label="현재가 및 전일 대비">
<div className="vtd-target-quote-left">
@@ -34,8 +34,6 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
[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;
if (rows.length === 0) {
return (
@@ -49,17 +47,12 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
<div
className={[
'vtd-sig-panel-wrap',
isDetail ? '' : 'vtd-sig-panel-wrap--summary',
isDetail ? 'vtd-sig-panel-wrap--detail' : 'vtd-sig-panel-wrap--summary',
receiving ? 'vtd-sig-panel-wrap--receiving' : '',
className,
].filter(Boolean).join(' ')}
>
{isDetail && evalCount > 0 && (
<div className="vtd-focus-sig-meta">
<span className="vtd-card-met-count">{metCount}/{evalCount} </span>
</div>
)}
<div className={`vtd-sig-panel${isDetail ? '' : ' vtd-sig-panel--summary'}`}>
<div className={`vtd-sig-panel${isDetail ? ' vtd-sig-panel--detail-top' : ' vtd-sig-panel--summary'}`}>
<div className="vtd-sig-panel-title">SIGNAL INTELLIGENCE &amp; MATCH RATES</div>
<div className="vtd-sig-visual">
<div className="vtd-sig-visual-eq">
@@ -68,8 +61,12 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
<VirtualConditionList metrics={metrics} />
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
</div>
{isDetail && <VirtualIndicatorCompareTable metrics={metrics} />}
</div>
{isDetail && (
<div className="vtd-sig-detail-table" aria-label="지표별 상태 목록">
<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>