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

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
@@ -0,0 +1,143 @@
/**
* 투자분석·대시보드 공통 카드 UI (brd-*)
*/
import React from 'react';
export const pct = (v: number, dec = 2) =>
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%` : '';
export const pctAbs = (v: number, dec = 1) =>
isFinite(v) && v !== 0 ? `${(v * 100).toFixed(dec)}%` : '';
export const num = (v: number, dec = 2) =>
isFinite(v) && v !== 0 ? v.toFixed(dec) : '';
export 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 + '원';
};
export const fmtKrwShort = (n: number) =>
n >= 1e8 ? `${(n / 1e8).toFixed(2)}` : n >= 1e4 ? `${(n / 1e4).toFixed(0)}` : n.toLocaleString('ko-KR');
export const colorCls = (v: number) => (v > 0 ? 'brd-pos' : v < 0 ? 'brd-neg' : '');
export 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>
);
}
export 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>
);
}
export 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>
);
}
export function WinRateCircle({ winRate, winning, total, centerLabel = '승률' }: {
winRate: number;
winning: number;
total: number;
centerLabel?: string;
}) {
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)">{centerLabel}</text>
</svg>
<div className="brd-donut-detail">
<span className="brd-pos"> {winning}</span>
<span className="brd-neg"> {total - winning}</span>
</div>
</div>
);
}
export 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>
);
}
export function ResourceBar({ label, pctFill, cls }: { label: string; pctFill: number; cls: string }) {
const w = Math.min(100, Math.max(0, pctFill));
return (
<div className="brd-pnl-row">
<span className={`brd-pnl-label ${cls}`}>{label}</span>
<div className="brd-pnl-track">
<div className={`brd-pnl-bar ${cls === 'brd-pos' ? 'brd-pnl-bar--pos' : 'brd-pnl-bar--neg'}`} style={{ width: `${w}%` }} />
</div>
<span className={`brd-pnl-val ${cls}`}>{w.toFixed(0)}%</span>
</div>
);
}