전체화면 모드 메뉴 상단으로 위치이동
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user