Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
178 lines
6.6 KiB
TypeScript
178 lines
6.6 KiB
TypeScript
/* =============================================================================
|
||
* 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<MasterRows> {
|
||
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<typeof L>[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,
|
||
],
|
||
});
|
||
}
|
||
|
||
/** 트리 접는 칸 — 브라우저 기본 <details> 로 둠. */
|
||
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] });
|
||
}
|