전체화면 모드 메뉴 상단으로 위치이동
This commit is contained in:
@@ -8,23 +8,19 @@ import React, { useState } from 'react';
|
||||
import type { BacktestAnalysis, BacktestSignal, BacktestStats } from '../utils/backendApi';
|
||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||
import { repairUtf8Mojibake } from '../utils/textEncoding';
|
||||
import {
|
||||
pct,
|
||||
pctAbs,
|
||||
num,
|
||||
wonFmt,
|
||||
colorCls,
|
||||
SectionTitle,
|
||||
KpiCard,
|
||||
MetricRow,
|
||||
WinRateCircle,
|
||||
CompareBar,
|
||||
} from './shared/analysisDashboardUi';
|
||||
|
||||
// ── 포맷 유틸 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const pct = (v: number, dec = 2) =>
|
||||
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%` : '–';
|
||||
const pctAbs = (v: number, dec = 1) =>
|
||||
isFinite(v) && v !== 0 ? `${(v * 100).toFixed(dec)}%` : '–';
|
||||
const num = (v: number, dec = 2) =>
|
||||
isFinite(v) && v !== 0 ? v.toFixed(dec) : '–';
|
||||
const wonFmt = (v: number) => {
|
||||
if (!isFinite(v) || v === 0) return '–';
|
||||
const abs = Math.abs(Math.round(v));
|
||||
const s = abs >= 100_000_000 ? `${(abs/100_000_000).toFixed(2)}억`
|
||||
: abs >= 10_000 ? `${(abs/10_000).toFixed(1)}만`
|
||||
: abs.toLocaleString();
|
||||
return (v >= 0 ? '+' : '-') + s + '원';
|
||||
};
|
||||
const fmtDate = (ts: number) => {
|
||||
const d = new Date(ts * 1000);
|
||||
return `${d.getFullYear()}.${String(d.getMonth()+1).padStart(2,'0')}.${String(d.getDate()).padStart(2,'0')}`;
|
||||
@@ -34,11 +30,6 @@ const fmtDateStr = (iso: string) => {
|
||||
return `${d.getFullYear()}.${String(d.getMonth()+1).padStart(2,'0')}.${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||||
};
|
||||
|
||||
const colorCls = (v: number) => v > 0 ? 'brd-pos' : v < 0 ? 'brd-neg' : '';
|
||||
const colorStyle = (v: number): React.CSSProperties => ({
|
||||
color: v > 0 ? 'var(--brd-pos)' : v < 0 ? 'var(--brd-neg)' : undefined,
|
||||
});
|
||||
|
||||
// ── Props ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface BacktestResultModalProps {
|
||||
@@ -65,106 +56,6 @@ export interface BacktestDashboardProps {
|
||||
reportMode?: boolean;
|
||||
}
|
||||
|
||||
// ── 섹션 제목 ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SectionTitle({ icon, title, right }: { icon: string; title: string; right?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="brd-section-title">
|
||||
<span className="brd-section-icon">{icon}</span>
|
||||
<span className="brd-section-text">{title}</span>
|
||||
{right && <span className="brd-section-right">{right}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── KPI 카드 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function KpiCard({ icon, label, value, sub, valueColor, accent }:
|
||||
{ icon: string; label: string; value: string; sub: string; valueColor?: string; accent?: string }) {
|
||||
return (
|
||||
<div className={`brd-kpi-card ${accent ? `brd-kpi-card--${accent}` : ''}`}>
|
||||
<div className="brd-kpi-top">
|
||||
<span className="brd-kpi-icon">{icon}</span>
|
||||
<span className="brd-kpi-label">{label}</span>
|
||||
</div>
|
||||
<div className={`brd-kpi-value ${valueColor ?? ''}`}>{value}</div>
|
||||
<div className="brd-kpi-sub">{sub}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 미니 지표 행 (테이블 스타일) ─────────────────────────────────────────
|
||||
|
||||
function MetricRow({ label, value, cls, note }: { label: string; value: string; cls?: string; note?: string }) {
|
||||
return (
|
||||
<div className="brd-metric-row">
|
||||
<span className="brd-metric-label">
|
||||
{label}
|
||||
{note && <span className="brd-metric-note">{note}</span>}
|
||||
</span>
|
||||
<span className={`brd-metric-value ${cls ?? ''}`}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 승률 도넛 (CSS 원) ────────────────────────────────────────────────────
|
||||
|
||||
function WinRateCircle({ winRate, winning, total }: { winRate: number; winning: number; total: number }) {
|
||||
const r = 28;
|
||||
const circ = 2 * Math.PI * r;
|
||||
const pct2 = Math.max(0, Math.min(1, winRate));
|
||||
const dash = pct2 * circ;
|
||||
return (
|
||||
<div className="brd-donut-wrap">
|
||||
<svg width="72" height="72" viewBox="0 0 72 72">
|
||||
<circle cx="36" cy="36" r={r} fill="none" stroke="var(--bg4)" strokeWidth="8"/>
|
||||
<circle
|
||||
cx="36" cy="36" r={r} fill="none"
|
||||
stroke={pct2 >= 0.5 ? 'var(--brd-pos)' : 'var(--brd-neg)'}
|
||||
strokeWidth="8"
|
||||
strokeDasharray={`${dash} ${circ}`}
|
||||
strokeLinecap="round"
|
||||
strokeDashoffset={circ * 0.25}
|
||||
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}
|
||||
/>
|
||||
<text x="36" y="33" textAnchor="middle" fontSize="11" fontWeight="800" fill="var(--text)">{pctAbs(winRate)}</text>
|
||||
<text x="36" y="45" textAnchor="middle" fontSize="8.5" fill="var(--text3)">승률</text>
|
||||
</svg>
|
||||
<div className="brd-donut-detail">
|
||||
<span className="brd-pos">▲ {winning}</span>
|
||||
<span className="brd-neg">▼ {total - winning}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 수평 비교 바 ─────────────────────────────────────────────────────────
|
||||
|
||||
function CompareBar({ labelA, valA, labelB, valB, colorA, colorB }:
|
||||
{ labelA: string; valA: number; labelB: string; valB: number; colorA: string; colorB: string }) {
|
||||
const max = Math.max(Math.abs(valA), Math.abs(valB), 0.01);
|
||||
const pctA2 = Math.min(100, Math.abs(valA) / max * 100);
|
||||
const pctB2 = Math.min(100, Math.abs(valB) / max * 100);
|
||||
return (
|
||||
<div className="brd-compare">
|
||||
<div className="brd-compare-row">
|
||||
<span className="brd-compare-label">{labelA}</span>
|
||||
<div className="brd-compare-track">
|
||||
<div className="brd-compare-bar" style={{ width: `${pctA2}%`, background: colorA }}/>
|
||||
</div>
|
||||
<span className="brd-compare-val" style={{ color: colorA }}>{pct(valA)}</span>
|
||||
</div>
|
||||
<div className="brd-compare-row">
|
||||
<span className="brd-compare-label">{labelB}</span>
|
||||
<div className="brd-compare-track">
|
||||
<div className="brd-compare-bar" style={{ width: `${pctB2}%`, background: colorB }}/>
|
||||
</div>
|
||||
<span className="brd-compare-val" style={{ color: colorB }}>{pct(valB)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 시그널 타입 ───────────────────────────────────────────────────────────
|
||||
|
||||
const SIG_LABEL: Record<string, string> = {
|
||||
|
||||
Reference in New Issue
Block a user