/* ============================================================================= * M02_MasterTemplete_UI_Side_Precombo.ts * 좌측 「일위대가 사전조합」 컨테이너(50-3 ③ · 51-4 · 134-3) — 구조물 조합 아래 · 층 단추(페이지가 만듦 · * 페이지 층과 따로 · 마스터 · 회사 · 내 개인) + 그 층 사전조합 나무(구분 › 상세구분 2단 폴더 · 빈 구분 = 미분류). * 문서 누름 = 편집 칸(UI_Main 종류 precombo = 일위대가 상세 모드) · 켜진 층 단추 다시 누름 = 목록표 * (`Library_List` · 생성 · 복사 · 삭제는 거기) · 그 층 주인(`owner`) · 고칠 권한(`canWrite`)은 고름에 실어 보냄. * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { createTreeRow, renderTree, type TreeNode } from "@ui/ui_template_tree"; import { listTemplates, type Kind, type Layer } from "./M02_MasterTemplete_Api_Fetch"; import type { Root } from "./M02_MasterTemplete_Layer"; import type { StructDef } from "./StructOptions/M02_SO_Def_Model"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { Selection } from "./M02_MasterTemplete_UI_Main"; export type PrecomboKind = Extract; export const PRECOMBO_TITLE = "일위대가 사전조합"; export const isPrecombo = (kind: Kind): kind is PrecomboKind => kind === "precombo"; /** 여러 구조물에 쓰는 묶음(콘크리트 · 거푸집 등) — 군마다 첫 상세구분(134-5) */ export const COMMON = "공통"; /** 사전조합 구분 · 상세구분(134-5) — 구조물군 A~G › 그 군 구조물 종류 + 「공통」(원천 `구조물정의.json` · * B05 구조물 배치와 같은 갈래 · 꺼진 종류도 넣음) */ export const precomboClasses = ( def: Pick, ): { name: string; details: string[] }[] => Object.entries(def.구조물군) .filter(([key]) => /^[A-G]$/.test(key)) .map(([key, name]) => ({ name, details: [COMMON, ...new Set(def.types.filter((t) => t.group === key).map((t) => t.name))], })); /** 사전조합 층(134-3) — 페이지 층과 따로 · `owner` null = 자기 층 */ export interface PrecomboScope { root: Root; layer: Layer; owner: string | null; canWrite: boolean; } /** 그 층 목록표 고름(켜진 층 단추 다시 누름) */ export const precomboList = (scope: PrecomboScope): Selection => ({ layer: scope.layer, owner: scope.owner, canWrite: scope.canWrite, projectId: null, kind: "precombo", name: "", label: tc("Lib_PrecomboAll"), }); export interface PrecomboOptions { scope: () => PrecomboScope; /** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) */ bar: () => HTMLElement; confirmLeave: () => Promise; onOpen: (sel: Selection | null) => void; getOpen: () => Selection | null; } export interface PrecomboHandle { body: HTMLElement; /** 고름 표시만 */ paint: () => void; /** 층 단추 · 나무를 그 층 목록으로 다시 그림 */ refresh: () => Promise; } interface Row { 이름: string; 종류: string; 제목?: string | null; 구분?: string | null; 상세구분?: string | null; } /** 줄 → 구분 › 상세구분 2단 나무(빈 구분 = 미분류 · 맨 뒤 · 빈 상세구분 = 구분 바로 아래) */ export function groupRows( rows: Row[], unsorted: string, ): { 구분: string; 상세: Map }[] { const out = new Map>(); for (const row of rows) { const top = row.구분 || ""; const sub = row.구분 ? row.상세구분 || "" : ""; if (!out.has(top)) out.set(top, new Map()); const subs = out.get(top)!; subs.set(sub, [...(subs.get(sub) ?? []), row]); } const order = (a: string, b: string): number => !a ? 1 : !b ? -1 : a.localeCompare(b, "ko-KR", { numeric: true }); return [...out.keys()].sort(order).map((top) => ({ 구분: top || unsorted, 상세: new Map([...out.get(top)!].sort(([a], [b]) => order(a, b))), })); } export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { const barHost = el("div"); const treeHost = el("div", { className: "ui-tree", attrs: { "data-precombo-tree": "" } }); const leaves = new Map(); const closed = new Set(); // 접은 폴더 — 다시 그려도 그대로 let rows: Row[] = []; let seq = 0; const open = async (row: Row): Promise => { const at = opt.scope(); if (!(await opt.confirmLeave())) return; opt.onOpen({ ...precomboList(at), name: row.이름, label: row.제목 ? `${row.이름} ${row.제목}` : row.이름, }); }; const leaf = (row: Row): TreeNode => ({ id: `doc:${row.이름}`, label: row.제목 || row.이름, tip: row.이름, count: null, run: () => void open(row), }); const folder = (id: string, label: string, children: TreeNode[]): TreeNode => ({ id, label, count: children.length, folder: true, open: !closed.has(id), children, }); const draw = (): void => { leaves.clear(); if (!rows.length) return void treeHost.replaceChildren( el("p", { className: "m02-side__empty", text: tc("Lib_None") }), ); const nodes = groupRows(rows, tc("Lib_Unsorted")).map((g) => folder( `sub:${g.구분}`, g.구분, [...g.상세].flatMap(([sub, list]) => sub ? [folder(`sub:${g.구분}/${sub}`, sub, list.map(leaf))] : list.map(leaf), ), ), ); treeHost.replaceChildren( ...renderTree(nodes, (node, caret, depth, onClick) => { const row = createTreeRow(node, caret, depth, () => { onClick(); if (!node.children?.length) return; if (row.querySelector(".ui-tree__caret")?.textContent === "▸") closed.add(node.id); else closed.delete(node.id); }); if (node.run) { row.dataset.precomboKey = node.id.slice(4); leaves.set(node.id.slice(4), row); } return row; }), ); paint(); }; const paint = (): void => { const cur = opt.getOpen(); const at = opt.scope(); const mine = cur && isPrecombo(cur.kind) && cur.layer === at.layer && (cur.owner ?? null) === at.owner; for (const [key, row] of leaves) row.classList.toggle("is-active", !!mine && cur.name === key); }; const refresh = async (): Promise => { const mine = ++seq; const at = opt.scope(); barHost.replaceChildren(opt.bar()); treeHost.replaceChildren(el("p", { className: "m02-side__empty", text: tc("Loading") })); let got: Row[] = []; try { got = (await listTemplates(at.layer, null, at.owner)).filter((r) => r.종류 === "precombo"); } catch { got = []; // 못 받으면 빈 나무(목록표에서 까닭) } if (mine !== seq) return; rows = got; draw(); }; const body = el("div", { className: "m02-side__items ui-sidebar-list", attrs: { "data-precombo": "" }, children: [barHost, treeHost], }); return { body, paint, refresh }; }