/* 아리 — 작업 리스크 레이더 (스마트 PMO) 지연 위험 · 업무 쏠림 · 의존성 영향을 현재 작업 트리에서 직접 계산해 경고한다. window.TaskRisk = { RiskRadar } */ (function () { const TD = window.TaskData; const TODAY = 8; // 6월 8일 const RP = { radar: "M19.07 4.93A10 10 0 0 0 6.99 3.34|M4 6h.01|M2.29 9.62a10 10 0 1 0 19.02-1.27|M16.24 7.76a6 6 0 1 0 1.07 7.04|M12 18h.01|M17.99 11.66a6 6 0 0 1-2.22 5.01|M12 12a2 2 0 1 0 4 0 2 2 0 0 0-4 0z|M13.41 10.59l5.66-5.66", clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 7v5l3 2", scale: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2|M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z|M22 21v-2a4 4 0 0 0-3-3.87|M16 3.13a4 4 0 0 1 0 7.75", link: "M6 3v12|M18 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M15 12H9a3 3 0 0 0-3 3", chev: "M9 6l6 6-6 6", }; function RI({ d, w }) { return ( ); } function walkAll(nodes, fn) { nodes.forEach((n) => { fn(n); if (n.children && n.children.length) walkAll(n.children, fn); }); } function findByTitle(nodes, title) { let hit = null; walkAll(nodes, (n) => { if (!hit && n.title === title) hit = n; }); return hit; } const dueD = (due) => (due ? parseInt(due.split("-")[1], 10) : 99); const dueTxt = (due) => (due ? `6/${dueD(due)}` : ""); /* 제목 기반 의존성 — 저장된 트리(로컬)에도 안전하게 매칭 */ const DEPS = [ { blocker: "예산 섹션 작성", blocked: "경영진 검토 요청 메일" }, { blocker: "데이터 전처리 파이프라인", blocked: "사용자 인터뷰 5건 정리" }, ]; function computeRisks(tasks) { const risks = []; const statusLabel = { todo: "시작 전", doing: "진행 중", waiting: "대기 중", review: "검토 중" }; /* ① 지연 위험 — 오늘까지 마감인데 미완료 */ tasks .filter((t) => t.status !== "done" && t.due && dueD(t.due) <= TODAY) .slice(0, 1) .forEach((t) => { risks.push({ kind: "지연 위험", icon: "clock", tone: "coral", id: t.id, text: <>{t.title} — 오늘({dueTxt(t.due)}) 마감인데 아직 {statusLabel[t.status]}이에요>, cta: "작업 열기", }); }); /* ② 업무 쏠림 — 담당자별 미완료 작업 수 편차 */ const counts = {}; walkAll(tasks, (n) => { if (n.status !== "done" && n.assignee) counts[n.assignee] = (counts[n.assignee] || 0) + 1; }); const entries = Object.entries(counts).filter(([w]) => TD.people[w]); if (entries.length > 1) { entries.sort((a, b) => b[1] - a[1]); const [topWho, topN] = entries[0]; const avg = entries.reduce((s, [, n]) => s + n, 0) / entries.length; if (topN >= avg * 1.5 && topN >= 4) { const p = TD.people[topWho]; risks.push( p.me ? { kind: "업무 쏠림", icon: "scale", tone: "amber", text: <>미완료 작업 {topN}건이 내게 몰려 있어요 — 팀 평균의 {Math.round((topN / avg) * 10) / 10}배. 벅찬 작업은 위임 & 추적으로 넘겨보세요>, } : { kind: "업무 쏠림", icon: "scale", tone: "amber", text: <>{p.name}님에게 미완료 작업 {topN}건이 몰려 있어요 — 평균의 {Math.round((topN / avg) * 10) / 10}배, 일부 재배분을 추천드려요>, } ); } } /* ③ 의존성 영향 — 선행 작업이 안 끝나면 밀리는 후속 작업 */ for (const d of DEPS) { const a = findByTitle(tasks, d.blocker), b = findByTitle(tasks, d.blocked); if (a && b && a.status !== "done" && b.status !== "done") { risks.push({ kind: "의존성", icon: "link", tone: "violet", id: b.id, text: <>{d.blocker}이(가) 늦어지면 {d.blocked}{b.due ? `(${dueTxt(b.due)})` : ""}까지 함께 밀려요>, cta: "후속 작업 보기", }); break; // 가장 임박한 1건만 } } return risks.slice(0, 3); } function RiskRadar({ tasks, onOpen }) { const risks = React.useMemo(() => computeRisks(tasks), [tasks]); if (!risks.length) return null; return (
{r.text}