// frontend/components/tasks/DetailPanel.tsx — 작업 상세 드로어 (포커스 드릴다운) "use client"; import { Fragment, useEffect, useState } from "react"; import { Icon } from "@/components/Icon"; import { cx } from "@/lib/cx"; import { COLUMNS, STATUS_META, dueDay, findPath, flattenTasks, stat } from "@/lib/tasks/tree"; import type { Person, Project, Status, Task } from "@/lib/types"; import { Av, Tag } from "./bits"; import { Comments } from "./Comments"; import { RichNotes } from "./RichNotes"; import { ScaffoldPanel } from "./ScaffoldPanel"; import { SubRow } from "./SubRow"; /** 같은 프로젝트를 함께 하는 비-본인 담당자 중 최다 인원 제안 (원본 suggestCollaborator) */ function suggestCollaborator(tasks: Task[], projectId: string): string | null { if (!projectId || projectId === "me") return null; // 개인 프로젝트 제외 const counts: Record = {}; for (const n of flattenTasks(tasks)) { if (n.assignee_id && n.assignee_id !== "jiwoo" && n.project_id === projectId) { counts[n.assignee_id] = (counts[n.assignee_id] || 0) + 1; } } const ppl = Object.keys(counts); if (!ppl.length) return null; ppl.sort((a, b) => counts[b] - counts[a]); return ppl[0]; } interface Props { tasks: Task[]; focusId: string; rootId: string | null; people: Record; projects: Record; me: Person; onFocus: (id: string) => void; onClose: () => void; onCheck: (id: string) => void; onField: (id: string, patch: Partial) => void; onAddChild: (parentId: string, title: string) => void; onScaffolded: () => void; onDelegate: (id: string, who: string) => void; onDelete: (id: string) => void; onNotes: (id: string, html: string) => void; onAddComment: (id: string, text: string) => void; } export function DetailPanel(p: Props) { const [menu, setMenu] = useState(false); const [expanded, setExpanded] = useState>({}); const [adding, setAdding] = useState(false); const [subText, setSubText] = useState(""); const path = findPath(p.tasks, p.focusId) ?? []; const node = path[path.length - 1]; useEffect(() => { setAdding(false); setMenu(false); }, [p.focusId]); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === "Escape") p.onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [p]); if (!node) return null; const rootIdx = p.rootId ? path.findIndex((n) => n.id === p.rootId) : -1; const crumbs = rootIdx >= 0 ? path.slice(rootIdx) : path; const person = node.assignee_id ? p.people[node.assignee_id] : null; const s = stat(node); const sm = STATUS_META[node.status]; const proj = p.projects[node.project_id]; const comments = node.comments || []; const onExpand = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] })); const submitSub = () => { if (subText.trim()) p.onAddChild(node.id, subText.trim()); setSubText(""); setAdding(false); }; const collabWho = node.assignee_id === "jiwoo" && node.status !== "done" ? suggestCollaborator(p.tasks, node.project_id) : null; return ( <>
); }