전체화면 모드 메뉴 상단으로 위치이동

This commit is contained in:
Macbook
2026-06-03 14:14:02 +09:00
parent ba592b0789
commit 4dc7962a2f
9 changed files with 836 additions and 351 deletions
+12 -121
View File
@@ -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> = {