전략편집기 수정

This commit is contained in:
Macbook
2026-05-25 17:56:25 +09:00
parent aac6454724
commit 02d14e4b2b
39 changed files with 1974 additions and 288 deletions
@@ -1,5 +1,6 @@
import React from 'react';
import type { ConditionStatus } from '../../utils/virtualSignalMetrics';
import { coerceFiniteNumber } from '../../utils/safeFormat';
interface Props {
status: ConditionStatus;
@@ -22,10 +23,8 @@ const ConditionStatusSignal: React.FC<Props> = ({ status, matchRate, progress, c
return <span className="vtd-cond-signal vtd-cond-signal--unknown"></span>;
}
const rate = matchRate ?? progress;
const pct = rate != null && Number.isFinite(rate)
? `${Math.round(rate)}%`
: null;
const rate = coerceFiniteNumber(matchRate ?? progress);
const pct = rate != null ? `${Math.round(rate)}%` : null;
return (
<span className={`vtd-cond-signal vtd-cond-signal--${status}${compact ? ' vtd-cond-signal--compact' : ''}`}>
@@ -1,6 +1,7 @@
import React from 'react';
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
import { resolveHeatTier } from '../../utils/virtualSignalMetrics';
import { coerceFiniteNumber } from '../../utils/safeFormat';
interface Props {
metrics: ConditionMetric[];
@@ -41,7 +42,7 @@ const VirtualConditionList: React.FC<Props> = ({ metrics }) => {
<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 pct = Math.min(100, Math.max(0, coerceFiniteNumber(matchRate) ?? 0));
const minFill = pct > 0 && tier === 'nomatch' ? 6 : pct > 0 ? 6 : 0;
return (
<li key={row.id} className={`vtd-heat-row vtd-heat-row--${tier}`}>
@@ -2,6 +2,7 @@ import React from 'react';
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
import { formatStrategyThreshold, resolveHeatTier } from '../../utils/virtualSignalMetrics';
import { formatIndicatorValue } from '../../utils/virtualStrategyConditions';
import { coerceFiniteNumber } from '../../utils/safeFormat';
import ConditionStatusSignal from './ConditionStatusSignal';
interface Props {
@@ -30,7 +31,8 @@ const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
</thead>
<tbody>
{metrics.map(({ row, status, matchRate }) => {
const tier = resolveHeatTier(matchRate, status);
const rate = coerceFiniteNumber(matchRate) ?? 0;
const tier = resolveHeatTier(rate, status);
return (
<tr key={row.id}>
<td className="vtd-sig-table-ind">{row.displayName}</td>
@@ -43,13 +45,13 @@ const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
<div className="vtd-sig-row-bar vtd-sig-row-bar--heat">
<div
className={`vtd-sig-row-bar-fill vtd-sig-row-bar-fill--${tier}`}
style={{ width: `${matchRate}%` }}
style={{ width: `${rate}%` }}
/>
</div>
<span className="vtd-sig-table-pct">{`${matchRate}%`}</span>
<span className="vtd-sig-table-pct">{`${rate}%`}</span>
</td>
<td className="vtd-sig-table-status-cell">
<ConditionStatusSignal status={status} matchRate={matchRate} compact />
<ConditionStatusSignal status={status} matchRate={rate} compact />
</td>
</tr>
);
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react';
import { coerceFiniteNumber } from '../../utils/safeFormat';
const SEGMENTS = 25;
@@ -7,9 +8,10 @@ interface Props {
}
const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
const rate = coerceFiniteNumber(matchRate) ?? 0;
const litCount = useMemo(
() => Math.round((Math.min(100, Math.max(0, matchRate)) / 100) * SEGMENTS),
[matchRate],
() => Math.round((Math.min(100, Math.max(0, rate)) / 100) * SEGMENTS),
[rate],
);
const segments = useMemo(() => {
@@ -20,7 +22,7 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
items.push({ lit: false, tone: 'blue' });
continue;
}
if (matchRate >= 100 && i === SEGMENTS - 1) {
if (rate >= 100 && i === SEGMENTS - 1) {
items.push({ lit: true, tone: 'peak' });
} else if (i >= Math.floor(SEGMENTS * 0.55)) {
items.push({ lit: true, tone: 'gold' });
@@ -29,7 +31,7 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
}
}
return items;
}, [litCount, matchRate]);
}, [litCount, rate]);
const ticks = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0];
@@ -53,7 +55,7 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
/>
))}
</div>
{matchRate >= 100 && (
{rate >= 100 && (
<div className="vtd-sig-eq-badge">100% MATCH</div>
)}
</div>
@@ -111,9 +111,9 @@ const VirtualTargetCard: React.FC<Props> = ({
<span className="vtd-card-sym">{sym}</span>
</div>
<label className="vtd-card-strategy-field" onClick={e => e.stopPropagation()}>
<span className="vtd-card-strategy-label"></span>
<select
className="vtd-card-strategy-select"
aria-label="투자전략"
value={strategyId ?? globalStrategyId ?? ''}
onChange={e => {
const v = e.target.value;
@@ -70,9 +70,9 @@ const VirtualTargetFocusView: React.FC<Props> = ({
<span className="vtd-card-sym">{sym}</span>
</div>
<label className="vtd-card-strategy-field" onClick={e => e.stopPropagation()}>
<span className="vtd-card-strategy-label"></span>
<select
className="vtd-card-strategy-select"
aria-label="투자전략"
value={strategyId ?? globalStrategyId ?? ''}
onChange={e => {
const v = e.target.value;
@@ -1,27 +1,19 @@
import React, { memo, useEffect, useRef, useState } from 'react';
import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker';
function toFiniteNumber(v: unknown): number | null {
if (v == null || v === '') return null;
const n = typeof v === 'number' ? v : Number(v);
return Number.isFinite(n) ? n : null;
}
import { coerceFiniteNumber, safePercentFromRate, safeToFixed } from '../../utils/safeFormat';
function fmtKrw(price: number | null | undefined | unknown): string {
const p = toFiniteNumber(price);
const p = coerceFiniteNumber(price);
if (p == null) return '-';
if (p >= 1_000_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
if (p >= 1_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
if (p >= 1) return p.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
if (p >= 0.01) return p.toFixed(4);
return p.toFixed(8);
if (p >= 0.01) return safeToFixed(p, 4, '-');
return safeToFixed(p, 8, '-');
}
function fmtPct(rate: number | null | undefined | unknown): string {
const r = toFiniteNumber(rate);
if (r == null) return '-';
const sign = r >= 0 ? '+' : '';
return `${sign}${(r * 100).toFixed(2)}%`;
return safePercentFromRate(rate, 2).replace('—', '-');
}
const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir }) {