// 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) =>
)}
{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
{c.value} {c.unit}
{c.label}
{c.sub}
; })}
); } 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}
{trendCard}
{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.label}
{c.value} {c.unit} · {c.sub}
; }); } Object.assign(window, { Main, SECTOR_COLORS });