Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts
T

173 lines
5.6 KiB
TypeScript

/* =============================================================================
* 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 {
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<string, () => Promise<unknown>>;
// 표 부품(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<T>(mods: Mods, name: string): Promise<T | null> {
for (const load of Object.values(mods)) {
try {
const mod = (await load()) as Record<string, unknown>;
if (typeof mod[name] === "function") return mod[name] as T;
} catch {
// 못 읽는 파일은 건너뜀 — 페이지는 뜸
}
}
return null;
}
export interface MainHandle {
root: HTMLElement;
open: (sel: Selection | null) => Promise<void>;
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<void> {
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<void> {
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.문서;
await mount(got.문서, 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<void> {
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 };
}