// frontend/lib/tasks/tree.ts — 트리 유틸 (무한 중첩) import type { Folder, Project, Task } from "@/lib/types"; /* ---- 작업 트리 ---- */ export function findPath(nodes: Task[], id: string, trail: Task[] = []): Task[] | null { for (const n of nodes) { const t = [...trail, n]; if (n.id === id) return t; if (n.children?.length) { const r = findPath(n.children, id, t); if (r) return r; } } return null; } export const getNode = (nodes: Task[], id: string): Task | null => { const p = findPath(nodes, id); return p ? p[p.length - 1] : null; }; export function editTree(nodes: Task[], id: string, fn: (n: Task) => Task): Task[] { return nodes.map((n) => n.id === id ? fn(n) : n.children?.length ? { ...n, children: editTree(n.children, id, fn) } : n, ); } export function removeFromTree(nodes: Task[], id: string): Task[] { return nodes .filter((n) => n.id !== id) .map((n) => (n.children?.length ? { ...n, children: removeFromTree(n.children, id) } : n)); } export const stat = (node: Task) => { const c = node.children ?? []; return { total: c.length, done: c.filter((x) => x.status === "done").length }; }; export const pct = (node: Task) => { const s = stat(node); return s.total ? Math.round((s.done / s.total) * 100) : 0; }; /* ---- 마감 표시 (ISO date 기준) ---- */ export const dueDay = (due: string | null): number => (due ? new Date(due).getUTCDate() : 99); export const dueLabel = (due: string | null): string => { if (!due) return "—"; const d = new Date(due); return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`; // "6/8" }; export const TODAY = 8; // 6월 8일 — 리스크/임박 기준 (CONTRACT 고정) export const isSoon = (t: Task) => t.status !== "done" && dueDay(t.due) <= TODAY; /* ---- 모든 노드 평탄화 (top-level + 하위작업) ---- */ export function flattenTasks(nodes: Task[]): Task[] { const out: Task[] = []; const walk = (ns: Task[]) => { for (const n of ns) { out.push(n); if (n.children?.length) walk(n.children); } }; walk(nodes); return out; } /* ---- 프로젝트 인덱스 (buildIndex 이식) ---- */ export interface ProjIndexRec { id: string; name: string; tone: string; folder: boolean; parentId: string | null; depth: number; hasChildren: boolean; } export function buildIndex(folders: Folder[]) { const projects: ProjIndexRec[] = []; const byId: Record = {}; const walk = (nodes: (Project | Folder)[], parentId: string | null, depth: number) => { for (const n of nodes) { const children = (n as Project).children ?? (n as Folder).projects ?? []; const rec: ProjIndexRec = { id: n.id, name: n.name, tone: (n as Project).tone ?? "faint", folder: "is_system" in n, parentId, depth, hasChildren: children.length > 0, }; projects.push(rec); byId[n.id] = rec; if (children.length) walk(children, n.id, depth + 1); } }; walk(folders, null, 0); const isDescendant = (pid: string, ancestorId: string): boolean => { let cur: ProjIndexRec | undefined = byId[pid]; while (cur) { if (cur.id === ancestorId) return true; cur = cur.parentId ? byId[cur.parentId] : undefined; } return false; }; const pathOf = (id: string): ProjIndexRec[] => { const out: ProjIndexRec[] = []; let cur: ProjIndexRec | undefined = byId[id]; while (cur) { out.unshift(cur); cur = cur.parentId ? byId[cur.parentId] : undefined; } return out; }; return { projects, byId, isDescendant, pathOf }; } /* ---- 상태 메타 (라벨/accent) ---- */ export const STATUS_META: Record = { 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)" }, }; export const COLUMNS = [ { id: "todo", label: "할 일", accent: "var(--faint)" }, { id: "doing", label: "진행 중", accent: "var(--blue)" }, { id: "waiting", label: "대기 중", accent: "var(--violet)" }, { id: "review", label: "검토", accent: "var(--coral)" }, { id: "done", label: "완료", accent: "var(--green)" }, ] as const;