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.
106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
// frontend/components/tasks/SubRow.tsx — 재귀 하위작업 행 (드릴다운)
|
|
"use client";
|
|
import { Icon } from "@/components/Icon";
|
|
import { cx } from "@/lib/cx";
|
|
import { dueLabel, stat } from "@/lib/tasks/tree";
|
|
import type { Person, Task } from "@/lib/types";
|
|
import { Av } from "./bits";
|
|
|
|
interface Props {
|
|
node: Task;
|
|
depth: number;
|
|
people: Record<string, Person>;
|
|
expanded: Record<string, boolean>;
|
|
onExpand: (id: string) => void;
|
|
onCheck: (id: string) => void;
|
|
onFocus: (id: string) => void;
|
|
}
|
|
|
|
export function SubRow({ node, depth, people, expanded, onExpand, onCheck, onFocus }: Props) {
|
|
const has = !!(node.children && node.children.length);
|
|
const open = !!expanded[node.id];
|
|
const s = stat(node);
|
|
const done = node.status === "done";
|
|
return (
|
|
<>
|
|
<div
|
|
className={cx("subrow", done && "done")}
|
|
style={{ paddingLeft: 6 + depth * 22 }}
|
|
onClick={() => onFocus(node.id)}
|
|
>
|
|
{has ? (
|
|
<button
|
|
className={cx("sub-twist", open && "open")}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onExpand(node.id);
|
|
}}
|
|
aria-label="펼치기"
|
|
>
|
|
<Icon name="chev" />
|
|
</button>
|
|
) : (
|
|
<span className="sub-twist-sp" />
|
|
)}
|
|
<div
|
|
className={cx("sub-check", done && "done")}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCheck(node.id);
|
|
}}
|
|
role="checkbox"
|
|
aria-checked={done}
|
|
aria-label="완료 토글"
|
|
>
|
|
<Icon name="tick" w={3} />
|
|
</div>
|
|
<span className="sub-title">{node.title}</span>
|
|
<div className="sub-meta">
|
|
{has && (
|
|
<span className="sub-chip">
|
|
<Icon name="branch" /> {s.done}/{s.total}
|
|
</span>
|
|
)}
|
|
{node.est && (
|
|
<span className="sub-chip est">
|
|
<Icon name="clock" /> {node.est}
|
|
</span>
|
|
)}
|
|
{node.due && (
|
|
<span className="sub-chip">
|
|
<Icon name="cal" /> {dueLabel(node.due)}
|
|
</span>
|
|
)}
|
|
<Av person={node.assignee_id ? people[node.assignee_id] : null} />
|
|
<button
|
|
className="sub-drill"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onFocus(node.id);
|
|
}}
|
|
aria-label="작업 열기"
|
|
>
|
|
<Icon name="chev" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{has && open && (
|
|
<div className="subkids">
|
|
{node.children.map((c) => (
|
|
<SubRow
|
|
key={c.id}
|
|
node={c}
|
|
depth={depth + 1}
|
|
people={people}
|
|
expanded={expanded}
|
|
onExpand={onExpand}
|
|
onCheck={onCheck}
|
|
onFocus={onFocus}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|