Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
180 lines
6.6 KiB
TypeScript
180 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 { valueWithUnit, type OverrideState } from "./Z01_MasterData_UI_Cells";
|
||
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}`,
|
||
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",
|
||
searchable: false,
|
||
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: Record<OverrideState, Parameters<typeof L>[0]> = {
|
||
meaning_changed: "Z01_MasterData_List_MeaningChanged",
|
||
source_changed: "Z01_MasterData_List_SourceChanged",
|
||
orphan: "Z01_MasterData_List_Orphan",
|
||
edited: "Z01_MasterData_List_Edited",
|
||
};
|
||
|
||
/**
|
||
* 고친 것 목록을 표 한 벌로 — 정렬(원본 바뀜 → 주인 없음 → 고쳐짐)과 쪽 나눔은 서버가 함.
|
||
* 화면이 다시 줄 세우면 맨 위가 그 쪽 안에서만 맨 위가 됨(2026-09-15 브레인 ②). 서버에 검색이 없어 검색 칸은 숨김.
|
||
*/
|
||
async function loadOverrides(query: RowsQuery): Promise<MasterRows> {
|
||
const { items, total } = await fetchOverrides(query);
|
||
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.map((item) => ({
|
||
...item,
|
||
value: valueWithUnit(item.value, item.unit),
|
||
original: valueWithUnit(item.original, item.unit),
|
||
...(Object.hasOwn(item, "current")
|
||
? { current: valueWithUnit(item.current, item.unit) }
|
||
: {}),
|
||
// state 가 안 오면 때우지 않고 드러냄(서버 버그).
|
||
state: L(STATE_LABEL[item.state] ?? "Z01_MasterData_State_Missing"),
|
||
kind: L(`Z01_MasterData_Base_${item.kind}` as const),
|
||
})),
|
||
total,
|
||
};
|
||
}
|
||
|
||
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] });
|
||
}
|