/* 아리 — 작업(Tasks) · 클린 화이트 글래스 상단바=메인메뉴 · 좌측 사이드바=서브메뉴(중첩 프로젝트) · 칸반/리스트/캘린더 */ const { useState, useEffect, useRef } = React; const TD = window.TaskData; const PROJ = Object.fromEntries(TD.projects.map((p) => [p.id, p])); const PPL = TD.people; /* ---------- 아이콘 ---------- */ const P = { spark: "M12 3l1.9 5.2L19 10l-5.1 1.8L12 17l-1.9-5.2L5 10l5.1-1.8z|M19 4v3|M5 17v3", grid: "M3 3h7v7H3z|M14 3h7v7h-7z|M14 14h7v7h-7z|M3 14h7v7H3z", cal: "M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z", route: "M6 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M18 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M9 16h6a3 3 0 0 0 3-3V11", check: "M9 11l3 3L22 4|M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11", tick: "M20 6 9 17l-5-5", mail: "M22 7 13.03 12.7a1.94 1.94 0 0 1-2.06 0L2 7|M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z", brain: "M12 5a3 3 0 1 0-5.9.8A3 3 0 0 0 4 9.5 3 3 0 0 0 6 14a3 3 0 0 0 6 1zM12 5a3 3 0 1 1 5.9.8A3 3 0 0 1 20 9.5 3 3 0 0 1 18 14a3 3 0 0 1-6 1z|M12 5v14", heart: "M19 14c1.5-1.5 3-3.4 3-5.5A4.5 4.5 0 0 0 12 5.5 4.5 4.5 0 0 0 2 8.5c0 2.1 1.5 4 3 5.5l7 7z", list: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01", inbox: "M22 12h-6l-2 3h-4l-2-3H2|M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z", search: "M21 21l-4.34-4.34|M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", bell: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9|M10.3 21a1.94 1.94 0 0 0 3.4 0", sun: "M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10z|M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4", moon: "M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z", plus: "M12 5v14M5 12h14", x: "M18 6 6 18M6 6l12 12", more: "M5 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM11 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM17 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0z", chev: "M9 6l6 6-6 6", star: "M12 2.5l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 18.8 6.2 21l1.1-6.5L2.6 9.8l6.5-.9z", flag: "M4 22V4M4 4s1.5-1 5-1 5 2 8 2 4-1 4-1v10s-1.5 1-4 1-5-2-8-2-5 1-5 1", folder: "M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", hash: "M4 9h16M4 15h16M10 3L8 21M16 3l-2 18", arrow: "M5 12h14|M13 6l6 6-6 6", grip: "M9 5a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM9 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM9 19a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM14 5a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM14 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM14 19a1 1 0 1 0 2 0 1 1 0 0 0-2 0z", file: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|M14 2v6h6M9 13h6M9 17h4", user: "M20 21a8 8 0 0 0-16 0|M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", bold: "M6 4h7a4 4 0 0 1 0 8H6z|M6 12h8a4 4 0 0 1 0 8H6z", italic: "M19 4h-9M14 20H5M15 4 9 20", under: "M6 4v6a6 6 0 0 0 12 0V4|M5 21h14", ulist: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01", olist: "M10 6h11M10 12h11M10 18h11|M4 4v4M3 4h1M3 8h2|M3 14h2l-2 3h2", quote: "M6 11H3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v8c0 2-1 3.5-3 4|M16 11h-3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v8c0 2-1 3.5-3 4", msg: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", send: "M22 2 11 13|M22 2 15 22l-4-9-9-4z", back: "M19 12H5|M12 19l-7-7 7-7", trash: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", branch: "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 6zM6 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M15 12H9a3 3 0 0 0-3 3", users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z|M23 21v-2a4 4 0 0 0-3-3.87|M16 3.13a4 4 0 0 1 0 7.75", clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 7v5l3 2", wand: "M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M17.8 6.2 19 5M3 21l9-9M12.2 6.2 11 5", zap: "M13 2 3 14h9l-1 8 10-12h-9z", spark2: "M12 3l1.9 5.2L19 10l-5.1 1.8L12 17l-1.9-5.2L5 10l5.1-1.8z|M19 4v3|M5 17v3", }; function I({ d, w }) { return ( ); } const MAIN = [ { id: "dash", label: "대시보드", icon: "grid", href: "대시보드.html" }, { id: "sbox", label: "인박스", icon: "inbox", href: "인박스.html" }, { id: "appr", label: "결재함", icon: "spark", badge: "3", href: "결재함.html" }, { id: "auto", label: "자동화", icon: "zap", href: "자동화.html" }, { id: "journey", label: "여정", icon: "route", href: "여정.html" }, { id: "cal", label: "일정", icon: "cal", href: "일정.html" }, { id: "task", label: "작업", icon: "check", badge: "4", href: "작업.html" }, { id: "mail", label: "메일", icon: "mail", href: "메일.html" }, { id: "noti", label: "알림", icon: "bell", badge: "6", href: "알림.html" }, { id: "research", label: "리서치", icon: "brain", href: "리서치.html" }, { id: "trip", label: "여행", icon: "send", href: "여행.html" }, { id: "life", label: "라이프", icon: "heart", href: "라이프.html" }, { id: "wrap", label: "하루 마감", icon: "moon", href: "하루 마감.html" }, ]; const VIEWS = [ { id: "kanban", label: "칸반", icon: "grid" }, { id: "list", label: "리스트", icon: "list" }, ]; const STATUS_META = { todo: { label: "할 일", c: "var(--faint)" }, doing: { label: "진행 중", c: "var(--blue)" }, waiting: { label: "대기 중", c: "var(--violet)" }, review: { label: "검토", c: "var(--coral)" }, done: { label: "완료", c: "var(--green)" }, }; function load(k, fb) { try { const v = localStorage.getItem("ariT." + k); return v === null ? fb : JSON.parse(v); } catch (e) { return fb; } } function save(k, v) { try { localStorage.setItem("ariT." + k, JSON.stringify(v)); } catch (e) {} } /* 작업 = 무한 중첩 트리. 하위 작업은 그 자체가 완전한 작업 노드. */ function findPath(nodes, id, trail) { trail = trail || []; for (const n of nodes) { const t = trail.concat(n); if (n.id === id) return t; if (n.children && n.children.length) { const r = findPath(n.children, id, t); if (r) return r; } } return null; } const getNode = (nodes, id) => { const p = findPath(nodes, id); return p ? p[p.length - 1] : null; }; function editTree(nodes, id, fn) { return nodes.map((n) => { if (n.id === id) return fn(n); if (n.children && n.children.length) return { ...n, children: editTree(n.children, id, fn) }; return n; }); } function removeFromTree(nodes, id) { return nodes.filter((n) => n.id !== id).map((n) => (n.children && n.children.length ? { ...n, children: removeFromTree(n.children, id) } : n)); } const stat = (node) => { const c = node.children || []; return { total: c.length, done: c.filter((x) => x.status === "done").length }; }; const pct = (node) => { const s = stat(node); return s.total ? Math.round((s.done / s.total) * 100) : 0; }; const dueDay = (due) => (due ? parseInt(due.split("-")[1], 10) : 99); const dueLabel = (due) => { if (!due) return "—"; const [m, d] = due.split("-"); return `${+m}/${+d}`; }; /* 위임 추천 대상 — 아리는 동료의 업무량을 모른다. 대신 '이 작업이 속한 프로젝트를 함께 하는 사람'을 내 작업 데이터에서 찾는다. */ function suggestCollaborator(tasks, projectId) { if (!projectId || projectId === "me") return null; // 개인 프로젝트는 위임 제안 안 함 const counts = {}; (function walk(ns) { ns.forEach((n) => { if (n.assignee && n.assignee !== "jiwoo" && n.project === projectId) { counts[n.assignee] = (counts[n.assignee] || 0) + 1; } if (n.children && n.children.length) walk(n.children); }); })(tasks); const people = Object.keys(counts); if (!people.length) return null; people.sort((a, b) => counts[b] - counts[a]); return { who: people[0], sameProject: true }; } function Av({ who }) { const p = PPL[who]; if (!p) return null; return