/* ============================================================================= * M02_MasterTemplete_Labor.ts * 좌측 「일위대가 조합」 컨테이너(구조물 조합 위 · PLAN 27-2b) — 「일위대가 라이브러리」 줄 아래 층 단추 * (`TreePanel_Roots`) · 조합 나무(폴더 [구분, 상세구분] 깊이 2 · 구조물 라이브러리와 같은 `TreePanel` · * 좌측 패널 안 · PLAN 29-2) · 줄 · 나무 항목을 누르면 메인 칸에 찾기 칸 + 조합 목록 · 상세(`Labor_Edit`). * 나무 고침 = `POST /api/m02/layers/{층}/combos/tree` · 「조합 더하기」 = 그 폴더를 채운 빈 조합(저장해야 생김). * ========================================================================== */ import { el, showConfirmDialog } from "@ui/ui_template_elements"; import { ApiError } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; import { editComboTree, fetchComboList, type ComboList, type ComboTreeEdit, type LaborScope, } from "./M02_MasterTemplete_Labor_Api"; import { createLaborEditor } from "./M02_MasterTemplete_Labor_Edit"; import { noteOf, toView } from "./M02_MasterTemplete_Labor_Tree"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { createTreePanel } from "./M02_MasterTemplete_TreePanel"; /** 나무 부품이 아는 막힘으로 — 판 다름 = `StaleError("HTTP 409")` · 나머지는 서버 까닭 글 */ function treeError(error: unknown): Error { if (!(error instanceof ApiError)) return error instanceof Error ? error : new Error(String(error)); const d = error.detail as { errors?: string[]; stale?: unknown } | string | null; const text = typeof d === "string" ? d : d?.errors?.join(" · ") || `HTTP ${String(error.status)}`; return error.status === 409 ? new StaleError(text) : new Error(text); } export interface LaborLibraryOptions { scope: LaborScope; canEdit: boolean; /** 나무 항목을 누름 — 메인 칸에 일위대가 화면을 먼저 띄움 */ reveal: () => Promise; } export interface LaborLibrary { /** 메인 칸 — 찾기 칸 + 조합 목록 · 상세 */ root: HTMLElement; /** 좌측 패널 나무 */ tree: HTMLElement; /** 나무 맨 위 더하기 「+」(층 단추 옆) */ add: HTMLElement; refresh: () => Promise; /** 메인 칸 「저장 안 한 고침」 손잡이 */ bind: (setDirty: (dirty: boolean) => void) => void; } export function buildLaborLibrary(opt: LaborLibraryOptions): LaborLibrary { const { scope } = opt; let setDirty: (dirty: boolean) => void = () => {}; let keys = new Set(); // 지금 목록의 조합 키 — 열린 조합이 지워졌나 const took = (list: ComboList): ComboList => { keys = new Set(list.줄.map((c) => c.키)); editor.setList(list.줄); return list; }; const editor = createLaborEditor({ scope, canEdit: opt.canEdit, onDirty: (dirty) => setDirty(dirty), onSaved: async (key) => { await tree.refresh(); tree.select(key); }, }); const tree = createTreePanel({ layer: scope.layer, owner: scope.owner, canEdit: opt.canEdit, maxDepth: 2, item: { add: tc("AddCombo"), name: tc("ComboName"), onAdd: (path) => editor.startNew(path) }, api: { fetch: async () => { return toView(took(await fetchComboList(scope))); }, edit: async (_layer, edit, 판) => { const body = { ...edit, 판 } as ComboTreeEdit; const got = await editComboTree(body, scope).catch((error: unknown) => { throw treeError(error); }); return toView(took(got), noteOf(edit, got.지운조합)); }, }, onPick: async (item) => { await opt.reveal(); tree.select(item.key); void editor.open(item.key); }, // 나무를 고치기 전 — 저장 안 한 조합 고침은 버릴지 물음(버리면 목록으로) settle: async () => { if (!editor.isDirty()) return "clean"; if (!(await showConfirmDialog(tc("BackAsk")))) return false; editor.showList(); return "changed"; }, // 나무가 바뀜 — 열린 조합은 새 판으로 다시 엶(이름 · 폴더가 바뀌었을 수 있음) · 지워졌으면 목록 onChanged: async () => { const open = editor.current(); if (!open) return; if (keys.has(open)) await editor.open(open); else editor.showList(); }, }); // 찾기 칸 하나(PLAN 29-6) — 나무와 조합 목록을 같은 글 · 같은 규칙으로 거름 tree.search .querySelector("input") ?.addEventListener("input", (event) => editor.setQuery((event.target as HTMLInputElement).value), ); return { root: el("div", { className: "m02-labor", children: [tree.search, editor.root] }), tree: tree.root, add: tree.add, refresh: () => tree.refresh(), bind: (fn) => (setDirty = fn), }; } export interface LaborSectionOptions { /** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */ show: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ confirmLeave: () => Promise; /** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */ onOpen: () => void; /** 라이브러리 — `reveal` = 메인 칸에 일위대가 화면을 띄움(나무 항목을 누를 때) */ make: (reveal: () => Promise) => LaborLibrary; /** 층 단추(PLAN 29-2) — `add` = 나무 맨 위 더하기 「+」를 단추 옆에 */ bar?: (add: HTMLElement) => HTMLElement; } export interface LaborSection { title: string; body: HTMLElement; open: () => Promise; /** 다른 화면을 열었음 — 고름 표시만 지움 */ clear: () => void; } /** 컨테이너 몸 = 층 단추 + 나무(같은 이름 줄 단추 없음 · 컨테이너 제목을 펼치면 엶 · PLAN 29-6) */ export function buildLaborSection(opt: LaborSectionOptions): LaborSection { let opened = false; const mark = (on: boolean): void => { opened = on; }; async function open(): Promise { if (opened || !(await opt.confirmLeave())) return; library.bind(opt.show(tc("Library"), library.root)); opt.onOpen(); mark(true); await library.refresh(); } // 나무는 좌측 패널에 늘 있음(PLAN 29-2) — 처음부터 만들고 읽음 const library = opt.make(open); void library.refresh(); const body = el("div", { className: "m02-side__items ui-sidebar-list", children: [...(opt.bar ? [opt.bar(library.add)] : []), library.tree], }); return { title: tc("Title"), body, open, clear: () => mark(false) }; }