/* ============================================================================= * Z01_MasterData_UI_Side.ts * 좌측 패널 — B03~B09 와 같은 도킹 사이드바 안에 접히는 상자 둘(기초단가 · 마스터 트리) * * 상자 겉모습·접기는 공용(`ui-collapsible ui-sidebar-section`) · 누른 단추 하나만 강조 * (두 상자를 가로질러 한 개) (2026-09-15 브레인 — 좌측 패널 양식 갈음 · 기초단가 칸). * ========================================================================== */ import { attachCollapsible } from "@ui/ui_template_collapsible"; import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { BASE_PRICE_KINDS, fetchBasePrices, fetchMasterRows, fetchOverrides, type MasterGroup, type MasterRows, type RowsQuery, saveBasePrice, VALUES_TABLE_ID, } from "./Z01_MasterData_Api_Fetch"; import type { RowsView } from "./Z01_MasterData_UI_Rows"; export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLElement { const panel = el("div", { className: "z01-master__side" }); const activate = (button: HTMLElement): void => { panel.querySelector(".is-active")?.classList.remove("is-active"); button.classList.add("is-active"); }; const baseTitle = L("Z01_MasterData_BasePrices"); const baseGrid = el("div", { className: "z01-master__base-grid" }); for (const kind of BASE_PRICE_KINDS) { const label = L(`Z01_MasterData_Base_${kind}` as const); const button = el("button", { className: "z01-master__base", attrs: { type: "button" }, text: label, }); button.addEventListener("click", () => { activate(button); view.show({ title: `${baseTitle} › ${label}`, key: `base-prices/${kind}`, // 요율은 구간 줄에 코드가 없어 구간 이름으로 이음 — 법이 바뀌면 고친 값이 주인을 잃기 쉬움. notice: kind === "rate" ? L("Z01_MasterData_Rate_Notice") : undefined, load: (query) => fetchBasePrices(kind, query), save: (rowId, values) => saveBasePrice(kind, rowId, values), }); }); baseGrid.append(button); } const overridesButton = el("button", { className: "z01-master__base z01-master__base--wide", attrs: { type: "button" }, text: L("Z01_MasterData_Overrides"), }); overridesButton.addEventListener("click", () => { activate(overridesButton); view.show({ title: `${baseTitle} › ${L("Z01_MasterData_Overrides")}`, key: "overrides", load: loadOverrides, }); }); baseGrid.append(overridesButton); const tree = el("nav", { className: "z01-master__tree" }); for (const group of groups) { const groupBox = folder("z01-master__group", group.label, String(group.files.length)); groupBox.open = true; // 부산물은 계산이 남긴 기록 — 고칠 정본이 아님(2026-09-15 브레인 갈래 나눔표). if (group.key === "byproduct") { groupBox.append( el("p", { className: "z01-master__note", text: L("Z01_MasterData_Byproduct") }), ); } for (const file of group.files) { const fileBox = folder( "z01-master__file", file.label, file.label === file.key ? "" : file.key, ); // 낱값 마디는 표들 밑에 — 누르면 여느 표와 같은 길로 그림(2026-09-15 브레인). const ordered = [...file.tables].sort( (a, b) => Number(a.id === VALUES_TABLE_ID) - Number(b.id === VALUES_TABLE_ID), ); for (const table of ordered) { const button = el("button", { className: "z01-master__table", attrs: { type: "button", title: table.key }, children: [ el("span", { text: table.label }), el("span", { className: "z01-master__key", text: table.row_count.toLocaleString("ko-KR"), }), ], }); button.addEventListener("click", () => { activate(button); view.show({ title: `${file.label} › ${table.label}`, key: `${file.key} / ${table.key}`, load: (query) => fetchMasterRows(file.id, table.id, query), }); }); fileBox.append(button); } groupBox.append(fileBox); } tree.append(groupBox); } panel.append(section(baseTitle, baseGrid), section(L("Z01_MasterData_Tree"), tree)); attachCollapsible(panel); return panel; } const STATE_LABEL = { source_changed: "Z01_MasterData_List_SourceChanged", orphan: "Z01_MasterData_List_Orphan", edited: "Z01_MasterData_List_Edited", } as const; /** * 고친 것 목록을 표 한 벌로 — 서버 정렬(원본 바뀜 → 주인 없음 → 고쳐짐)을 그대로 두고 쪽만 자름. * 쪽을 서버에서 나눠 받으면 맨 위가 그 쪽 안에서만 맨 위가 됨(2026-09-15 브레인 ②). */ async function loadOverrides(query: RowsQuery): Promise { const needle = query.q.toLowerCase(); const items = (await fetchOverrides()).filter( (item) => !needle || JSON.stringify(item).toLowerCase().includes(needle), ); const start = (query.page - 1) * query.size; const column = (key: string, label: Parameters[0]) => ({ key, label: L(label) }); return { columns: [ column("state", "Z01_MasterData_List_State"), column("kind", "Z01_MasterData_BasePrices"), column("row_label", "Z01_MasterData_List_Row"), column("column_label", "Z01_MasterData_List_Column"), column("value", "Z01_MasterData_List_Value"), column("original", "Z01_MasterData_List_Original"), column("current", "Z01_MasterData_List_Current"), ], rows: items.slice(start, start + query.size).map((item) => ({ ...item, // state 가 안 오면 때우지 않고 드러냄(서버 버그). state: L(STATE_LABEL[item.state] ?? "Z01_MasterData_State_Missing"), kind: L(`Z01_MasterData_Base_${item.kind}` as const), })), total: items.length, }; } function section(title: string, body: HTMLElement): HTMLElement { return el("section", { className: "z01-master__section ui-collapsible ui-sidebar-section", children: [ el("p", { className: "z01-master__section-title ui-collapsible__title", text: title }), body, ], }); } /** 트리 접는 칸 — 브라우저 기본
로 둠. */ function folder(className: string, label: string, aside: string): HTMLDetailsElement { const summary = el("summary", { children: [ el("span", { text: label }), el("span", { className: "z01-master__key", text: aside }), ], }); return el("details", { className, children: [summary] }); }