/* ============================================================================= * M02_MasterTemplete_UI_Main.ts * 마스터 템플릿 메인 칸 — 양식 한 개를 읽어 표·도면 편집 부품에 붙임(계약 `6_계약.md` 화면 부품). * 부품 파일이 없거나 못 읽어도 페이지는 뜸 — 빈 자리 글. * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { readFilled, readTemplate, saveTemplate, StaleError, type Kind, type Layer, } from "./M02_MasterTemplete_Api_Fetch"; export interface Selection { layer: Layer; projectId: string | null; kind: Kind; name: string; } interface EditorHandle { getDoc: () => unknown; destroy: () => void; } type Mods = Record Promise>; // 표 부품(sub3) · 도면 부품(sub2) — 파일 이름은 각 창이 정함 → 폴더를 훑어 내보낸 이름으로 찾음 const SHEET_MODS: Mods = import.meta.glob("../ui_template/sheet/*.ts"); const DRAWING_MODS: Mods = import.meta.glob("./M02_MasterTemplete_Drawing*.ts"); async function findExport(mods: Mods, name: string): Promise { for (const load of Object.values(mods)) { try { const mod = (await load()) as Record; if (typeof mod[name] === "function") return mod[name] as T; } catch { // 못 읽는 파일은 건너뜀 — 페이지는 뜸 } } return null; } export interface MainHandle { root: HTMLElement; open: (sel: Selection | null) => Promise; current: () => Selection | null; } /** 이 층에서 고칠 수 있나 — 시스템은 관리자만 · 회사는 여기서 못 고침(공식으로 저장으로만) */ export function canEdit(layer: Layer, isAdmin: boolean): boolean { return layer === "project" || layer === "personal" || (layer === "system" && isAdmin); } export function createMain(isAdmin: boolean): MainHandle { const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") }); const reload = createButton({ label: L("M02_Reload"), variant: "ghost" }); const save = createButton({ label: L("M02_Save"), variant: "filled" }); const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } }); const host = el("div", { className: "m02-main__host" }); const root = el("div", { className: "m02-main", children: [ el("div", { className: "m02-main__head", children: [title, reload, save] }), notice, host, ], }); let sel: Selection | null = null; let version = ""; let loaded: unknown = null; let editor: EditorHandle | null = null; let seq = 0; const showNotice = (nodes: (HTMLElement | string)[]): void => { notice.replaceChildren(...nodes); notice.hidden = nodes.length === 0; }; const empty = (text: string): void => { host.replaceChildren(el("p", { className: "m02-main__empty", text })); }; const bar = (): void => { reload.hidden = !sel; save.hidden = !sel || !canEdit(sel.layer, isAdmin); }; const drop = (): void => { editor?.destroy(); editor = null; }; async function mount(doc: unknown, at: Selection): Promise { const readOnly = !canEdit(at.layer, isAdmin); host.replaceChildren(); if (at.kind === "table") { const create = await findExport<(h: HTMLElement, d: unknown, o: object) => EditorHandle>( SHEET_MODS, "createSheet", ); if (create) { editor = create(host, doc, { mode: at.layer === "project" ? "project" : "master" }); return; } return empty(L("M02_NoSheet")); } const mountDrawing = await findExport<(h: HTMLElement, d: unknown, o: object) => EditorHandle>( DRAWING_MODS, "mountDrawingTemplate", ); if (mountDrawing) { editor = mountDrawing(host, doc, { onSave: () => void doSave(), readOnly }); return; } empty(L("M02_NoDrawing")); } async function open(next: Selection | null): Promise { const mine = ++seq; drop(); showNotice([]); sel = next; bar(); if (!next) { title.textContent = L("M02_PickTemplate"); return empty(""); } title.textContent = next.name; try { const got = await readTemplate(next.layer, next.kind, next.name, next.projectId); if (mine !== seq) return; version = got.판 ?? ""; loaded = got.문서; // 프로젝트 층 표 — 작업본 판은 그대로 두고 보이는 문서만 설계값으로 채움(서버가 채움 · 저장 안 함) const shown = next.layer === "project" && next.kind === "table" && next.projectId ? await readFilled(next.projectId, next.name).then( (f) => f.문서, () => got.문서, ) : got.문서; if (mine !== seq) return; await mount(shown, next); } catch (error) { if (mine !== seq) return; empty(""); const why = error instanceof Error ? error.message : ""; showToast(L("M02_LoadFailed").replace("{value}", why), "error"); } } async function doSave(): Promise { if (!sel) return; const at = sel; try { const doc = editor ? editor.getDoc() : loaded; const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc); version = info.판; loaded = doc; showNotice([]); showToast(L("M02_Saved"), "success"); } catch (error) { if (error instanceof StaleError) { const again = createButton({ label: L("M02_Reload"), variant: "ghost" }); again.addEventListener("click", () => void open(at)); showNotice([L("M02_Stale"), again]); return; } const why = error instanceof Error ? error.message : ""; showToast(L("M02_SaveFailed").replace("{value}", why), "error"); } } save.addEventListener("click", () => void doSave()); reload.addEventListener("click", () => sel && void open(sel)); bar(); empty(""); return { root, open, current: () => sel }; }