/* ============================================================================= * M01_MasterData_UI_Page.ts * 마스터 요소 화면 — 좌측 도킹 패널(컨테이너 아홉: 인력·재료·기계·소요량·계수·환율·요율·일위대가 로직·로직 테스트) * / 우측 요소 표 · 표 목록 · 로직 편집 * * 시스템 관리자만. 고친 것은 초안(sessionStorage)에 쌓이고 [저장] 한 번에 서버로 — * 자동저장 없음. * ========================================================================== */ import "@ui/ui_template_workflow_layout.css"; import { createButton, createInputField, el, showConfirmDialog, showToast, } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { saveFiles } from "./M01_MasterData_Api_Fetch"; import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft"; import { renderRows } from "./M01_MasterData_UI_Rows"; import { buildSide, fileLabel, loadView, saveView, setPendingLogicKey, takePendingLogicKey, type Pick, } from "./M01_MasterData_UI_Side"; import { renderTables } from "./M01_MasterData_UI_Tables"; import type { LogicHandle } from "./M01_MasterData_UI_Logic_Page"; import "./M01_MasterData_UI_Style.css"; const TABLE_GROUPS = ["소요량", "계수"]; export async function renderM01MasterData(root: HTMLElement): Promise { const user = await fetchSessionUser().catch(() => null); if (user?.role !== "SYSTEM_ADMIN") { showToast(L("M01_AdminOnly"), "error"); navigateTo(ROUTES.B01_ACCOUNT); return; } root.innerHTML = ""; root.append(buildPage()); } function buildPage(): HTMLElement { let current: Pick | null = null; let query = ""; let dispose = (): void => {}; let logicMounted = false; let testMounted = false; /* --- 우측: 머리(제목·찾기·저장) + 알림 + 본문 --- */ const title = el("h2", { className: "m01-master__title", text: L("M01_PickFile") }); const searchField = createInputField({ type: "search", placeholder: L("M01_Search") }); const search = searchField.input; const summary = el("span", { className: "m01-master__summary" }); const save = createButton({ label: L("M01_Save"), variant: "filled" }); const drop = createButton({ label: L("M01_Discard"), variant: "ghost" }); const notice = el("div", { className: "m01-master__notice", attrs: { hidden: "" } }); const body = el("div", { className: "m01-master__body" }); const head = el("div", { className: "m01-master__head", children: [title, searchField.root, summary, drop, save], }); const logicHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } }); const testHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } }); const elementView = el("div", { className: "m01-master__elements", children: [head, notice, body], }); const showNotice = (nodes: (HTMLElement | string)[]): void => { notice.replaceChildren(...nodes); notice.hidden = nodes.length === 0; }; const refreshBar = (): void => { const n = totalCount(); summary.textContent = n ? L("M01_Changed").replace("{value}", String(n)) : ""; save.disabled = drop.disabled = n === 0; }; onDraftChange(refreshBar); refreshBar(); const showMode = (mode: "elements" | "logic" | "test"): void => { logicHost.hidden = mode !== "logic"; testHost.hidden = mode !== "test"; elementView.hidden = mode !== "elements"; }; const openFile = (pick: Pick): void => { dispose(); showMode("elements"); if (pick.view) { query = pick.view.q; search.value = query; } else saveView(pick.group, { q: query, page: 1 }); current = { ...pick, view: undefined }; const groupTitle = pick.group === pick.label ? [pick.group] : [pick.group, pick.label]; title.textContent = groupTitle.join(" · "); dispose = TABLE_GROUPS.includes(pick.group) ? renderTables(body, pick, query, openLogicTab) : renderRows(body, pick, query); showNotice(isStale(pick.file.file, pick.file.version) ? [L("M01_FileStale")] : []); }; let searchTimer: number | undefined; search.addEventListener("input", () => { window.clearTimeout(searchTimer); searchTimer = window.setTimeout(() => { query = search.value.trim(); if (current) openFile(current); }, 300); }); drop.addEventListener("click", async () => { if (!(await showConfirmDialog(L("M01_DiscardConfirm")))) return; discard(); showNotice([]); }); /** 저장·되받기 뒤 — 지금 열린 그룹의 새 판본으로 다시 그림 */ const reloadFiles = async (): Promise => { if (!current) return; const pick = current; try { const again = (await side.refresh(pick.group)).find((f) => f.file === pick.file.file); if (again) openFile({ ...pick, file: again, view: loadView(pick.group) }); } catch (error) { showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); } }; save.addEventListener("click", async () => { save.disabled = true; try { const result = await saveFiles(payload()); if (result.status === 200 && "files" in result) { discard(result.files.map((f) => f.file)); showNotice([]); showToast(L("M01_Saved"), "success"); await reloadFiles(); } else if (result.status === 409 && "stale" in result) { showNotice(staleNotice(result.stale)); } else if (result.status === 422 && "errors" in result) { showNotice([ el("strong", { text: L("M01_CheckErrors") }), ...result.errors.map((e) => el("p", { text: e })), ]); } else if ("detail" in result) { showNotice([L("M01_SaveFailed").replace("{value}", result.detail)]); } } catch (error) { showNotice([ L("M01_SaveFailed").replace("{value}", error instanceof Error ? error.message : ""), ]); } finally { refreshBar(); } }); const staleNotice = (stale: string[]): (HTMLElement | string)[] => { const again = createButton({ label: L("M01_StaleReload"), variant: "ghost" }); again.addEventListener("click", async () => { discard(stale); showNotice([]); await reloadFiles(); }); return [L("M01_Stale").replace("{value}", stale.map(fileLabel).join(", ")), again]; }; let logicHandle: LogicHandle | null = null; /** 로직 탭으로 전환 — `key` 가 있으면 그 로직을 바로 엶. */ const openLogicTab = (key?: string): void => { if (key) setPendingLogicKey(key); dispose(); dispose = (): void => {}; current = null; showMode("logic"); if (logicMounted) { const pending = takePendingLogicKey(); if (pending) void logicHandle?.openKey(pending); return; } logicMounted = true; void import("./M01_MasterData_UI_Logic_Page").then(async (m) => { const pending = takePendingLogicKey(); logicHandle = await m.mountM01Logic(logicHost, side, pending ?? undefined); }); }; /* --- 좌측: 컨테이너 아홉 --- */ const side = buildSide( openFile, () => openLogicTab(), () => { dispose(); dispose = (): void => {}; current = null; showMode("test"); if (testMounted) return; testMounted = true; void import("./M01_MasterData_UI_Test").then((m) => m.mountM01Test(testHost)); }, ); const layout = el("div", { className: "ui-workflow-layout m01-master" }); const main = el("main", { className: "ui-workflow-layout__main", children: [ el("div", { className: "m01-master__panel", children: [elementView, logicHost, testHost], }), ], }); const overlays = createWorkflowOverlays({ title: L("M01_Title"), optionsContent: el("div", { className: "m01-master__side", children: [side.root] }), showProjectName: false, onOptionsOpenChange: (isOpen) => layout.classList.toggle("is-options-open", isOpen), }); layout.append( el("div", { className: "ui-workflow-layout__body", children: [main] }), overlays.root, ); return layout; }