feat(M01): 왼쪽 패널 트리(전체 › 조사 · 원문 › 부문 › 장) · 표 끝 열 비고
- 인력·기계: 전체가 부모 · 조사/세부분류가 자식 · 줄 수 표시 (전체 408 = 묶음 합 · 기계 654 = 묶음 합) - 소요량·계수·로직: 원문 › 부문 › 장 트리 · 접기/펴기 - 로직 목록: 원문/장 선택칸 대신 트리로 범위 고름 - 표: 비고 열을 항상 끝에 보이게 · 눌러 고침 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Tree.ts
|
||||
* 왼쪽 패널 트리 — 부모(접기/펴기) · 자식(들여쓰기) · 줄 수 표시
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
|
||||
export interface TreeNode {
|
||||
id: string;
|
||||
label: string;
|
||||
/** 줄 수 — null 이면 안 보임 */
|
||||
count: number | null;
|
||||
/** 눌렀을 때 — 없으면 접기/펴기만 */
|
||||
run?: () => void;
|
||||
children?: TreeNode[];
|
||||
/** 처음부터 펴짐 */
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
/** 줄(버튼) 만들기 — 돌려받은 것 = 그 버튼 · click 은 부르는 쪽이 이음 */
|
||||
export type MakeRow = (
|
||||
node: TreeNode,
|
||||
caret: string,
|
||||
depth: number,
|
||||
onClick: () => void,
|
||||
) => HTMLElement;
|
||||
|
||||
export function renderTree(nodes: TreeNode[], make: MakeRow, depth = 0): HTMLElement[] {
|
||||
return nodes.map((node) => {
|
||||
if (!node.children?.length) return make(node, "", depth, () => node.run?.());
|
||||
const kids = el("div", {
|
||||
className: "m01-tree__kids",
|
||||
children: renderTree(node.children, make, depth + 1),
|
||||
});
|
||||
kids.hidden = !node.open;
|
||||
const row = make(node, kids.hidden ? "▸" : "▾", depth, () => {
|
||||
kids.hidden = !kids.hidden;
|
||||
const caret = row.querySelector(".m01-tree__caret");
|
||||
if (caret) caret.textContent = kids.hidden ? "▸" : "▾";
|
||||
node.run?.();
|
||||
});
|
||||
return el("div", { className: "m01-tree__branch", children: [row, kids] });
|
||||
});
|
||||
}
|
||||
|
||||
/** 파일 목록 → 원문 › 부문 › 장 — 장 이름이 「공통 03장 …」 이면 부문 「공통」 */
|
||||
export function bookTree<T extends { book: string | null; chapter: string }>(
|
||||
items: T[],
|
||||
leaf: (item: T, division: string) => TreeNode,
|
||||
id: (parts: string[]) => string,
|
||||
count: (list: T[]) => number,
|
||||
): TreeNode[] {
|
||||
const books = new Map<string, Map<string, T[]>>();
|
||||
for (const it of items) {
|
||||
const division = /^(\S+) \d+장/.exec(it.chapter)?.[1] ?? "";
|
||||
const divs = books.get(it.book ?? "") ?? new Map<string, T[]>();
|
||||
divs.set(division, [...(divs.get(division) ?? []), it]);
|
||||
books.set(it.book ?? "", divs);
|
||||
}
|
||||
return [...books].map(([book, divs]) => ({
|
||||
id: id([book]),
|
||||
label: book,
|
||||
count: count([...divs.values()].flat()),
|
||||
children: [...divs].flatMap(([division, list]) =>
|
||||
division
|
||||
? [
|
||||
{
|
||||
id: id([book, division]),
|
||||
label: division,
|
||||
count: count(list),
|
||||
children: list.map((it) => leaf(it, division)),
|
||||
},
|
||||
]
|
||||
: list.map((it) => leaf(it, "")),
|
||||
),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user