// Main.jsx — 종합 대시보드 (메인).
// 부문 = 조직 부문(투자금융부·프로젝트금융부·기업고객부)
const DEPT_COLORS = {};(typeof DEPTS !== 'undefined' ? DEPTS : []).forEach((d) => {DEPT_COLORS[d.name] = d.color;});
const SECTOR_COLORS = DEPT_COLORS; // alias kept for legend lookups
function useDeptRisk() {
return React.useMemo(() => DEPTS.map((d) => ({ key: d.name, label: d.name, value: d.risk, hold: d.hold, intensity: d.intensity, color: d.color })).
sort((a, b) => b.value - a.value), []);
}
function HeroRisk({ deltaAmt, deltaPct, expand }) {
if (!expand) {
return (
전체 위험액 (CVaR 99%, 1일)
{KPI.totalRisk.toLocaleString()}
억원
전일대비 {deltaAmt > 0 ? '+' : ''}{deltaAmt.toLocaleString()}억
보유 평가액
{(KPI.totalHold / 10000).toFixed(1)} 조원
위험액 비율
{(KPI.totalRisk / KPI.totalHold * 100).toFixed(2)} %
);
}
// expanded — fills the card width & height
const LIMIT = 52000;
const used = +(KPI.totalRisk / LIMIT * 100).toFixed(1);
const total = DEPTS.reduce((s, d) => s + d.risk, 0);
const stats = [
{ k: '보유 평가액', v: (KPI.totalHold / 10000).toFixed(1), u: '조원' },
{ k: '위험액 비율', v: (KPI.totalRisk / KPI.totalHold * 100).toFixed(2), u: '%' },
{ k: '위험 한도 소진율', v: used.toFixed(1), u: '%', kind: used >= 100 ? 'alert' : used >= 80 ? 'warn' : 'ok' }];
return (
전체 위험액
{KPI.totalRisk.toLocaleString()}
억원
전일대비 {deltaAmt > 0 ? '+' : ''}{deltaAmt.toLocaleString()}억
{stats.map((s, i) =>
{i > 0 && }
0 ? 18 : 0 }}>
{s.k}
{s.v} {s.u}
)}
부문별 위험액 분포
합계 {total.toLocaleString()}억
{DEPTS.map((d) =>
{d.name}
{d.risk.toLocaleString()}억
{(d.risk / total * 100).toFixed(1)}%
)}
);
}
function CountCards({ vertical }) {
const cards = [
{ label: '전체 보유 종목', value: KPI.accounts, unit: '종목', icon: 'briefcase', kind: 'neutral', sub: `정상 ${KPI.normal} · 채권 ${BONDS.length} · 주식 ${STOCKS.length}` },
{ label: '유보 종목', value: KPI.jubo, unit: '종목', icon: 'flag', kind: 'neutral', sub: '-15% 이상 손실 발생' },
{ label: '주의 종목', value: KPI.juui, unit: '종목', icon: 'alert', kind: 'warn', sub: '-12% 이상 손실 발생' },
{ label: '위험 종목', value: KPI.danger, unit: '종목', icon: 'alert', kind: 'alert', sub: '매도 권장 신호 발생' }];
return (
{cards.map((c) => {const s = STATUS[c.kind];
return
;
})}
);
}
function RiskViz({ mode, sectors }) {
const total = sectors.reduce((s, d) => s + d.value, 0);
if (mode === 'gauge') {
const LIMIT = 52000; // 위험액 한도(억)
const pct = +(total / LIMIT * 100).toFixed(1);
return (
= 100 ? 'alert' : pct >= 80 ? 'warn' : 'ok'} dot>{pct >= 100 ? '한도 초과' : pct >= 80 ? '한도 임박' : '한도 내 정상'}
위험액 한도
{LIMIT.toLocaleString()} 억
한도 잔여
{(LIMIT - total).toLocaleString()} 억
);
}
if (mode === 'heatmap') {
return (
({ label: s.label, value: s.value, intensity: s.intensity }))} h={196} />
);
}
return (
({ label: s.label, value: s.value, color: s.color, sub: `${(s.value / total * 100).toFixed(1)}% · 강도 ${(s.intensity * 100).toFixed(2)}%` }))} />
);
}
// 주요 시장 지표 (전일대비) — 리스크 요인 모니터링
function genSpark(seed, end, vol) {let r = seed >>> 0;const rnd = () => {r = Math.imul(r, 1103515245) + 12345 >>> 0;return r / 4294967296;};const n = 22,arr = new Array(n);let v = end;for (let i = n - 1; i >= 0; i--) {arr[i] = v;v = v * (1 + (rnd() - 0.5) * vol);}return arr;}
const MARKET = [
{ label: '美 국채 10년물', sub: 'UST 10Y', value: '4.28', num: 4.28, unit: '%', delta: 0.03, dunit: '%p', ddig: 2, seed: 11, vol: 0.012 },
{ label: '신용 스프레드', sub: 'IG OAS', value: '92', num: 92, unit: 'bp', delta: 2, dunit: 'bp', ddig: 0, seed: 23, vol: 0.030 },
{ label: '원/달러 환율', sub: 'USD/KRW', value: '1,382.5', num: 1382.5, unit: '원', delta: 4.2, dunit: '원', ddig: 1, seed: 37, vol: 0.006 },
{ label: 'SOFR 금리', sub: 'USD O/N', value: '4.31', num: 4.31, unit: '%', delta: -0.01, dunit: '%p', ddig: 2, seed: 53, vol: 0.008 }];
function MarketIndicators({ cols = 2 }) {
return (
{MARKET.map((m) => {const c = dirColor(m.delta);const series = genSpark(m.seed, m.num, m.vol);
return (
{m.label}
{m.sub}
{m.value}
{m.unit}
);
})}
);
}
// ── AI 리스크 리포트 (타이핑 생성 애니메이션) ──────────────────────────────
function AIReport({ deltaPct, deltaAmt }) {
const dir = deltaPct >= 0 ? '증가' : '감소';
const topDept = DEPTS.slice().sort((a, b) => b.risk - a.risk)[0];
const topPct = (topDept.risk / DEPTS.reduce((s, d) => s + d.risk, 0) * 100).toFixed(0);
// 두 단락 텍스트 (plain — bold 태그 없이 타이핑 후 하이라이트 오버레이)
const PARA1 = `간밤 미국 시장에서는 국채 10년물 금리가 4.28%로 소폭 상승하고 IG 신용 스프레드가 +2bp 확대되며 위험 선호가 다소 위축됐어요. SOFR 단기금리는 4.31%로 안정적이나, 원/달러 환율이 1,382.5원까지 오르며 환 변동성이 재차 확대되는 흐름이에요. 금리 상방 압력과 원화 약세가 동반되면서 보유 채권의 평가손과 해외자산 환산 부담이 함께 커질 수 있는 국면으로, 듀레이션이 긴 외화채권과 신용 민감 종목을 중심으로 시장 리스크가 누적되고 있어요.`;
const PARA2 = `포트폴리오 전체 위험액은 ${KPI.totalRisk.toLocaleString()}억으로 전일 대비 ${deltaPct >= 0 ? '+' : ''}${deltaPct}% (${deltaAmt > 0 ? '+' : ''}${deltaAmt.toLocaleString()}억) ${dir}했고, 부문별로는 ${topDept.name}의 위험액 비중이 ${topPct}%로 가장 높아 해외채권 변동성에 대한 민감도가 큰 상태예요. 위험 한도 소진율은 73.9% 수준이에요.`;
const FULL = PARA1 + '\n\n' + PARA2;
const SPEED = 28; // ms per char
const [visLen, setVisLen] = React.useState(0);
const [done, setDone] = React.useState(false);
React.useEffect(() => {
setVisLen(0); setDone(false);
const id = setInterval(() => {
setVisLen(n => {
const next = n + 3; // 3글자씩 진행
if (next >= FULL.length) { clearInterval(id); setDone(true); return FULL.length; }
return next;
});
}, SPEED);
return () => clearInterval(id);
}, []);
// visible 텍스트 분리 (단락 경계 기준)
const visible = FULL.slice(0, visLen);
const p1Visible = visible.slice(0, Math.min(visible.length, PARA1.length));
const p2Visible = visible.length > PARA1.length + 2
? visible.slice(PARA1.length + 2)
: '';
const showP2 = visible.length > PARA1.length;
const cursorInP1 = !showP2 && !done;
const cursorInP2 = showP2 && !done;
const Cursor = () => (
);
return (
AI 생성 완료
:
생성 중...
}
/>
{/* 단락 1 */}
{p1Visible}{cursorInP1 && }
{/* 구분선 — 단락1 완료 후 등장 */}
{showP2 &&
}
{/* 단락 2 */}
{showP2 && (
{p2Visible}{cursorInP2 && }
)}
{/* 주의 문구 — 완료 후 등장 */}
{done && (
본 리포트는 자동 생성된 참고 자료로, 투자 판단의 근거가 아니에요. · 기준 {FIRM.asOf}
)}
);
}
function Main({ t, openDetail }) {
const [view, setView] = React.useState(t.trendDefault || '부문별');
const heroLayout = t.mainLayout !== 'KPI 행형';
const deltaAmt = Math.round(TREND.total.at(-1) - TREND.total.at(-2));
const deltaPct = +((TREND.total.at(-1) / TREND.total.at(-2) - 1) * 100).toFixed(1);
const trendSeries = view === '부문별' ? TREND.dept : TREND.asset;
const trendColors = view === '부문별' ? DEPT_COLORS : { '해외채권': '#0A4FA3', '국내채권': '#00A0E0', '국내주식': '#E0922A', '해외주식': '#6D5BD0' };
const flaggedTable =
위험 {FLAGGED.filter(r=>r.status==='위험').length}
주의 {FLAGGED.filter(r=>r.status==='주의').length}
유보 {FLAGGED.filter(r=>r.status==='유보').length}
} />
openDetail(r)}
initialSort={null}
rows={FLAGGED.map((r, i) => ({ ...r, _key: r.isin + i }))}
cols={[
{ k: 'asset', t: '구분', w: 60, render: (r) => {r.asset}, sortVal: (r) => r.asset },
{ k: 'isin', t: 'ISIN', render: (r) => {r.isin} },
{ k: 'ledger', t: '원장번호', render: (r) => {r.ledger} },
{ k: 'name', t: '종목명', w: '19%', render: (r) => {r.name} },
{ k: 'ccy', t: '통화', w: 52, render: (r) => {r.ccy} },
{ k: 'type', t: '유형', render: (r) => {r.type} },
{ k: 'cvar', t: 'CVaR', num: true, render: (r) => r.pending ? 산출 보류 : {(r.cvar * 100).toFixed(2)}%, sortVal: (r) => r.cvar == null ? -Infinity : Math.abs(r.cvar) },
{ k: 'dod', t: 'CVaR 전일대비', num: true, render: (r) => r.pending ? — : , sortVal: (r) => r.dod == null ? 0 : r.dod },
{ k: 'status', t: '분류', w: 70, render: (r) => , sortVal: (r) => ({ 위험: 0, 주의: 1, 유보: 2 })[r.status] ?? 3 },
{ k: 'note', t: '사유', w: '15%', sortable: false, render: (r) => {r.note || '—'} }]
} />
;
const trendCard =
} />
{Object.keys(trendSeries).map((k) =>
{k}
)}
{view === '부문별' ? '기업고객부 위험액 상승 (해외채권 비중 영향)' : '해외채권 위험액 상승 (외화채 변동 영향)'}
;
const marketCard =
5분 지연} />
;
return (
{/* banner */}
점검 권고
전체 위험액은 전일 대비 = 0 ? T.up : T.down }}>{deltaPct >= 0 ? '+' : ''}{deltaPct}% ({deltaAmt > 0 ? '+' : ''}{deltaAmt.toLocaleString()}억) {deltaPct >= 0 ? '증가' : '감소'}했어요. 현재 위험 {KPI.danger}건 · 주의 {KPI.juui}건 · 유보 {KPI.jubo}건이 분류되어 있어요.
기준 {FIRM.asOf}
새로고침
{heroLayout ?
{marketCard}
{trendCard}
{flaggedTable}
:
{marketCard}
{flaggedTable}
}
);
}
// KPI-row variant: three compact count cards rendered as grid siblings
function CountCardsInline() {
const cards = [
{ label: '전체 보유 종목', value: KPI.accounts, unit: '종목', icon: 'briefcase', kind: 'neutral', sub: `정상 ${KPI.normal}` },
{ label: '유보 종목', value: KPI.jubo, unit: '종목', icon: 'flag', kind: 'neutral', sub: '-15% 손실' },
{ label: '주의 종목', value: KPI.juui, unit: '종목', icon: 'alert', kind: 'warn', sub: '-12% 손실' },
{ label: '위험 종목', value: KPI.danger, unit: '종목', icon: 'alert', kind: 'alert', sub: '매도 권장' }];
return cards.map((c) => {const s = STATUS[c.kind];
return
{c.value}
{c.unit} · {c.sub}
;
});
}
Object.assign(window, { Main, SECTOR_COLORS });