/* ============================================================================= * M02_MasterTemplete_UI_Side.ts * 마스터 템플릿 좌측 패널 — 시스템 양식 목록(표/도면) · 새로 · 본떠 · 지우기. 서버 길은 계약 `6_계약.md`. * ========================================================================== */ import { createButton, createInputField, createSelectField, el, showConfirmDialog, showToast, } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; import { deleteTemplate, listTemplates, readTemplate, saveTemplate, StaleError, type Kind, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; const KINDS: { kind: Kind; label: () => string }[] = [ { kind: "table", label: () => L("M02_KindTable") }, { kind: "drawing", label: () => L("M02_KindDrawing") }, ]; /** 새 양식의 첫 문서 — 표는 `3_표양식.md` 4장 틀 · 도면은 빈 웹캐드 문서 */ function stubDoc(kind: Kind, name: string): unknown { return kind === "table" ? { 양식: name, 종류: "표", 판: 1, 층: ["종류", "공법", "규격"], 열: [{ id: "sta", 머리: ["측점", null, null], 단위: null, 꼴: "글", 고정: true }], 줄: [], 합계줄: [], 보기: {}, } : { format: 6, entities: [] }; } export interface SideOptions { isAdmin: boolean; onOpen: (sel: Selection | null) => void; getOpen: () => Selection | null; /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ confirmLeave: () => Promise; } export interface SideHandle { root: HTMLElement; refresh: () => Promise; } /** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */ const badName = (name: string): boolean => /[\\/:*?"<>|\x00-\x1f]|\.\./.test(name) || name.startsWith("."); const why = (error: unknown): string => (error instanceof Error ? error.message : ""); const failed = (error: unknown): void => showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); export function buildSide(opt: SideOptions): SideHandle { const layer = "system" as const; let items: TemplateInfo[] = []; let seq = 0; const listHost = el("div", { className: "m02-side__lists" }); const btnNew = createButton({ label: L("M02_New"), variant: "filled" }); const btnCopy = createButton({ label: L("M02_Copy"), variant: "ghost" }); const btnDel = createButton({ label: L("M02_Delete"), variant: "danger" }); const editRow = el("div", { className: "m02-side__row", children: [btnNew, btnCopy, btnDel] }); const root = el("div", { className: "m02-side", children: [listHost, editRow] }); const isOpen = (info: TemplateInfo): boolean => { const cur = opt.getOpen(); return !!cur && cur.layer === layer && cur.kind === info.종류 && cur.name === info.이름; }; const currentInfo = (): TemplateInfo | undefined => { const cur = opt.getOpen(); return cur ? items.find((i) => i.종류 === cur.kind && i.이름 === cur.name) : undefined; }; function paintList(): void { listHost.replaceChildren( ...KINDS.map(({ kind, label }) => { const mine = items.filter((i) => i.종류 === kind); return el("div", { className: "m02-side__group", children: [ el("h4", { text: label() }), ...(mine.length ? mine.map((info) => { const b = el("button", { className: `m02-side__item${isOpen(info) ? " is-active" : ""}`, text: info.이름, attrs: { type: "button", title: info.수정일 }, }); b.addEventListener("click", () => void select(info)); return b; }) : [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]), ], }); }), ); } function paintButtons(): void { const editable = canEdit(layer, opt.isAdmin); btnNew.disabled = !editable; btnCopy.disabled = !editable || !currentInfo(); btnDel.disabled = !editable || !currentInfo(); } async function select(info: TemplateInfo): Promise { if (!isOpen(info) && !(await opt.confirmLeave())) return; opt.onOpen({ layer, projectId: null, kind: info.종류, name: info.이름 }); paintList(); paintButtons(); } async function refresh(): Promise { const mine = ++seq; paintButtons(); try { items = await listTemplates(layer, null); } catch (error) { items = []; showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error"); } if (mine !== seq) return; paintList(); paintButtons(); } /* --- 시스템 층 새로 · 본떠 · 지우기 --- */ const nameDialog = ( title: string, withKind: boolean, onOk: (kind: Kind, name: string) => Promise, ): void => { const name = createInputField({ label: L("M02_Name"), placeholder: L("M02_Name") }); const kind = createSelectField({ label: L("M02_Kind"), options: KINDS.map((k) => ({ value: k.kind, text: k.label() })), }); openModal({ title, closeLabel: L("M02_Close"), dialogClass: "m02-modal", mount: (body, close) => { const ok = createButton({ label: L("M02_Create"), variant: "filled" }); ok.addEventListener("click", async () => { const text = name.input.value.trim(); if (!text) return showToast(L("M02_NameNeeded"), "error"); if (badName(text)) return showToast(L("M02_BadName"), "error"); ok.disabled = true; // 연달아 눌러도 한 번만 try { await onOk(kind.select.value as Kind, text); close(); } catch (error) { if (error instanceof StaleError) showToast(L("M02_NameExists"), "error"); else failed(error); } finally { ok.disabled = false; } }); body.append(...(withKind ? [kind.root] : []), name.root, ok); name.input.focus(); }, }); }; const openCreated = async (kind: Kind, name: string): Promise => { showToast(L("M02_Created"), "success"); await refresh(); const info = items.find((i) => i.종류 === kind && i.이름 === name); if (info) await select(info); }; btnNew.addEventListener("click", () => nameDialog(L("M02_NewTitle"), true, async (kind, name) => { await saveTemplate("system", kind, name, null, "", stubDoc(kind, name)); await openCreated(kind, name); }), ); btnCopy.addEventListener("click", () => { const from = currentInfo(); if (!from) return; nameDialog(L("M02_CopyTitle"), false, async (_kind, name) => { const got = await readTemplate("system", from.종류, from.이름, null); await saveTemplate("system", from.종류, name, null, "", got.문서); await openCreated(from.종류, name); }); }); btnDel.addEventListener("click", async () => { await refresh(); // 저장 직후여도 최신 판으로 const info = currentInfo(); if (!info) return; if (!(await showConfirmDialog(L("M02_DeleteConfirm").replace("{value}", info.이름)))) return; try { await deleteTemplate(info.종류, info.이름, info.판); showToast(L("M02_Deleted"), "success"); opt.onOpen(null); await refresh(); } catch (error) { failed(error); } }); void refresh(); return { root, refresh }; }