// DeskStatus.jsx — 부서·담당자별 리스크 현황 (모니터링 탭 공용, 관리자/리스크 부서 뷰). function DeskStatus({ asset, onOpen }) { const people = peopleOf(asset); const depts = deptsOf(asset); const rows = React.useMemo(() => people.map((p) => { const m = summarize(p.pf); const lim = dealerLimit(p.emp); return { _key: p.emp, emp: p.emp, name: p.name, title: p.title, dept: p.dept, team: p.team, value: p.pf.value, ret: m.ret, risk: m.risk, limit: p.pf.limit, util: m.util, delta: Math.round(kospiDelta(p.pf)), lim, zc: stopCounts(p.pf) }; }), [asset]); const totalDelta = React.useMemo(() => Math.round(people.reduce((s, p) => s + kospiDelta(p.pf), 0)), [asset]); const deptCards = React.useMemo(() => depts.map((d) => { const m = summarize(deptPortfolio(d, asset)); const n = people.filter((p) => p.dept === d).length; return { d, n, risk: m.risk, util: m.util, value: m.value }; }), [asset]); const utilPill = (u) => u >= 100 ? 'alert' : u >= 80 ? 'warn' : 'ok'; return ( {asset === '주식' && 총 델타(KOSPI) {totalDelta.toLocaleString()}억} 기준 {FIRM.asOf} } /> {/* 부서 요약 */}
{deptCards.map((c) => (
{c.d} · {c.n}명
위험액 {Math.round(c.risk).toLocaleString()}억 · 평가 {fmtEok(c.value)}
{c.util.toFixed(0)}%
))}
onOpen({ emp: r.emp }) : undefined} rows={rows} cols={[ { k: 'name', t: '담당자', w: '15%', render: (r) => {r.name[0]} {r.name} {r.title} }, { k: 'dept', t: '부서 · 팀', w: '20%', render: (r) => {r.dept} · {r.team} }, { k: 'emp', t: '사번', render: (r) => {r.emp} }, { k: 'value', t: '평가액', num: true, render: (r) => {r.value.toLocaleString()}억, sortVal: (r) => r.value }, { k: 'ret', t: '수익률(연)', num: true, render: (r) => , sortVal: (r) => r.ret }, { k: 'risk', t: '위험액(CVaR)', num: true, render: (r) => {Math.round(r.risk).toLocaleString()}억, sortVal: (r) => r.risk }, ...(asset === '주식' ? [ { k: 'delta', t: '델타(KOSPI)', num: true, render: (r) => {r.delta.toLocaleString()}억, sortVal: (r) => r.delta }] : []), { k: 'stop', t: '손절 구간 도달', num: true, render: (r) => {const f = r.zc[1] + r.zc[2] + r.zc[3] + r.zc[4]; if (!f) return 정상; return {STOP_ZONES.slice(1).filter((z) => r.zc[z.v] > 0).map((z) => {z.l} {r.zc[z.v]})};}, sortVal: (r) => r.zc[4] * 1000 + r.zc[3] * 100 + r.zc[2] * 10 + r.zc[1] }, { k: 'sa', t: 'SA 한도', num: true, render: (r) => r.lim ? = 100 ? T.alert : limUtil(r.lim.sa) >= 80 ? T.warn : T.c600 }}>{r.lim.sa[0]}/{r.lim.sa[1]}억 ({limUtil(r.lim.sa).toFixed(0)}%) : , sortVal: (r) => r.lim ? limUtil(r.lim.sa) : 0 }, { k: 'inv', t: '투자한도', num: true, render: (r) => r.lim ? = 100 ? T.alert : limUtil(r.lim.invest) >= 80 ? T.warn : T.c600 }}>{limUtil(r.lim.invest).toFixed(0)}% : , sortVal: (r) => r.lim ? limUtil(r.lim.invest) : 0 }, ...(asset === '주식' ? [ { k: 'ratio', t: '비율한도(시총)', num: true, render: (r) => r.lim && r.lim.ratio ? = 100 ? T.alert : (r.lim.ratio.max / r.lim.ratio.cap * 100) >= 80 ? T.warn : T.c600 }}>{r.lim.ratio.max.toFixed(2)}/{r.lim.ratio.cap.toFixed(1)}% : , sortVal: (r) => r.lim && r.lim.ratio ? r.lim.ratio.max / r.lim.ratio.cap : 0 }] : []), { k: 'util', t: '한도 소진율', num: true, render: (r) => = 100 ? T.alert : r.util >= 80 ? T.warn : T.c800 }}>{r.util.toFixed(1)}%, sortVal: (r) => r.util }, { k: 'st', t: '상태', w: 86, render: (r) => {r.util >= 100 ? '한도 초과' : r.util >= 80 ? '한도 임박' : '정상'}, sortVal: (r) => -r.util }] } />
); } Object.assign(window, { DeskStatus });