// Bonds.jsx — 채권 메인. const DOM_COLORS = { '국채': '#0A4FA3', '공사·공단채': '#00A0E0', '특수채': '#6D5BD0', '금융채(은행채)': '#11865B' }; const COUNTRY_COLORS = ['#0A4FA3', '#00A0E0', '#6D5BD0', '#11865B', '#E0922A', '#C8363C', '#4F6272', '#8B5CF6', '#0E8C8C']; function fmtMat(d) {return d ? d.replace(/-/g, '.') : '—';} function ddayMat(d) {if (!d) return null;return Math.round((new Date(d) - new Date('2026-06-04')) / 86400000);} function Bonds({ openDetail }) { const p = PORTFOLIO.bond; const [q, setQ] = React.useState(''); const [region, setRegion] = React.useState('전체'); const [status, setStatus] = React.useState('전체'); const domComp = React.useMemo(() => Object.entries(DOM_COMP).sort((a, b) => b[1] - a[1]). map(([label, value]) => ({ label: label.replace('(은행채)', ''), value, color: DOM_COLORS[label] || '#94A3B8' })), []); const forComp = React.useMemo(() => Object.entries(FOR_COMP).sort((a, b) => b[1] - a[1]). map(([label, value], i) => ({ label, value, color: COUNTRY_COLORS[i % COUNTRY_COLORS.length] })), []); const domHold = Object.values(DOM_COMP).reduce((s, v) => s + v, 0); const forHold = Object.values(FOR_COMP).reduce((s, v) => s + v, 0); const domN = BONDS.filter((b) => !b.foreign).length,forN = BONDS.filter((b) => b.foreign).length; const statusCounts = { 전체: BONDS.length, 정상: BONDS.filter((b) => b.status === '정상').length, 유보: BONDS.filter((b) => b.status === '유보').length, 주의: BONDS.filter((b) => b.status === '주의').length, 위험: BONDS.filter((b) => b.status === '위험').length }; const rows = BONDS.filter((b) => { if (region === '국내' && b.foreign) return false; if (region === '해외' && !b.foreign) return false; if (status !== '전체' && b.status !== status) return false; if (q) {const s = q.toLowerCase();if (!(b.name.toLowerCase().includes(s) || b.isin.toLowerCase().includes(s) || String(b.ledger).toLowerCase().includes(s))) return false;} return true; }).map((b) => ({ ...b, _key: b.isin, dday: ddayMat(b.maturity), item: { asset: '채권', isin: b.isin, ledger: b.ledger, name: b.name, ccy: b.ccy, type: b.cat, cvar: b.cvar, dod: b.dod, status: b.status, pending: b.pending, bond: b } })); return (
{domN}종 · {(domHold / 10000).toFixed(1)}조} />
{forN}종 · {(forHold / 10000).toFixed(1)}조} />
} />
{rows.length}종 표시
openDetail(r.item)} initialSort={{ k: 'cvar', dir: 'desc' }} rows={rows} maxH={560} cols={[ { k: 'isin', t: 'ISIN', render: (r) => {r.isin} }, { k: 'ledger', t: '원장번호', render: (r) => {r.ledger} }, { k: 'name', t: '종목명', w: '20%', render: (r) => {r.foreign && }{r.name} }, { k: 'ccy', t: '통화', w: 54, render: (r) => {r.ccy} }, { k: 'cat', t: '유형', render: (r) => {r.foreign ? `외화채·${r.country}` : r.cat}, sortVal: (r) => r.cat }, { k: 'maturity', t: '만기', render: (r) => {const dd = r.dday;return {fmtMat(r.maturity)} {dd != null && (dd >= 0 ? D-{dd} : 경과)};}, sortVal: (r) => r.maturity }, { k: 'cvar', t: 'CVaR', num: true, render: (r) => r.pending ? 산출 보류 : = 0.06 ? T.warn : T.c800 }}>{(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: 72, render: (r) => , sortVal: (r) => ({ 위험: 0, 주의: 1, 유보: 2 })[r.status] ?? 3 }] } />
); } Object.assign(window, { Bonds, fmtMat, ddayMat, DOM_COLORS, COUNTRY_COLORS });