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
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;
|