You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

135 lines
4.3 KiB
TypeScript

// 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<string, ProjIndexRec> = {};
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<string, { label: string; c: string }> = {
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;