Files
Aislo/Z01_MasterData/Z01_MasterData_UI_Page.ts
T

286 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* Z01_MasterData_UI_Page.ts
* 마스터 데이터 — 좌 트리(갈래 → 파일 → 표) / 우 고른 표의 실제 줄(쪽 나누기 · 검색)
*
* 시스템 관리자만(서버도 require_system_admin 으로 다시 막음) · 읽기 전용부터
* (2026-09-15 브레인 Z01 — 마스터를 사람이 보고 고칠 수 있게 먼저 만들고 그 위에 일위대가 재조립).
* ========================================================================== */
import { ROUTES } from "@config/config_frontend";
import {
createButton,
createInputField,
el,
hideLoadingOverlay,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { createGeneralLayout } from "@ui/ui_template_general_layout";
import { t as L } from "@ui/ui_template_locale";
import { navigateTo } from "../A00_Common/router";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import {
fetchMasterRows,
fetchMasterTree,
type MasterFile,
type MasterGroup,
type MasterRows,
type MasterTable,
VALUES_TABLE_ID,
} from "./Z01_MasterData_Api_Fetch";
import {
clampPage,
columnTitle,
latestOnly,
pageCount,
rowCells,
shownColumns,
} from "./Z01_MasterData_UI_Cells";
import "./Z01_MasterData_UI_Style.css";
const PAGE_SIZE = 50;
const SEARCH_DELAY_MS = 300;
export async function renderZ01MasterData(root: HTMLElement): Promise<void> {
const user = await fetchSessionUser().catch(() => null);
if (user?.role !== "SYSTEM_ADMIN") {
showToast(L("Z01_MasterData_AdminOnly"), "error");
navigateTo(ROUTES.B01_ACCOUNT);
return;
}
showLoadingOverlay();
let groups: MasterGroup[] = [];
try {
groups = await fetchMasterTree();
} catch (error) {
showToast(error instanceof Error ? error.message : L("Z01_MasterData_LoadFailed"), "error");
} finally {
hideLoadingOverlay();
}
root.innerHTML = "";
root.append(buildPage(groups));
}
function buildPage(groups: MasterGroup[]): HTMLElement {
const view = buildRowsView();
const tree = buildTree(groups, view.pick);
const content = el("div", { className: "z01-master__content", children: [tree, view.root] });
return createGeneralLayout({
pageClass: "z01-master",
title: L("Z01_MasterData_Title"),
subtitle: L("Z01_MasterData_Subtitle"),
content,
}).root;
}
function buildTree(
groups: MasterGroup[],
pick: (file: MasterFile, table: MasterTable) => void,
): HTMLElement {
const nav = 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", () => {
nav.querySelector(".z01-master__table.is-active")?.classList.remove("is-active");
button.classList.add("is-active");
pick(file, table);
});
fileBox.append(button);
}
groupBox.append(fileBox);
}
nav.append(groupBox);
}
return nav;
}
/** 접는 칸 — 브라우저 기본 <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] });
}
interface RowsView {
root: HTMLElement;
pick: (file: MasterFile, table: MasterTable) => void;
}
function buildRowsView(): RowsView {
const state = {
file: null as MasterFile | null,
table: null as MasterTable | null,
page: 1,
q: "",
};
let showHidden = false;
let last: MasterRows | null = null;
let searchTimer = 0;
const fetchLatest = latestOnly(fetchMasterRows);
const title = el("h2", { className: "z01-master__title", text: L("Z01_MasterData_PickTable") });
const titleKey = el("span", { className: "z01-master__key" });
const search = createInputField({
type: "search",
placeholder: L("Z01_MasterData_Search"),
onInput: (value) => {
window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(() => {
state.q = value.trim();
state.page = 1;
void load();
}, SEARCH_DELAY_MS);
},
});
const hiddenToggle = el("input", { attrs: { type: "checkbox" } });
hiddenToggle.addEventListener("change", () => {
showHidden = hiddenToggle.checked;
draw();
});
const hiddenLabel = el("label", {
className: "z01-master__check",
children: [hiddenToggle, L("Z01_MasterData_ShowHidden")],
});
const grid = el("div", { className: "z01-master__grid-wrap" });
const pageInput = el("input", {
className: "ui-input z01-master__page-input",
attrs: { type: "number", min: "1", "aria-label": L("Z01_MasterData_Page") },
});
pageInput.addEventListener("change", () => goTo(Number(pageInput.value)));
const pageInfo = el("span", { className: "z01-master__page-info" });
// 큰 표(수만 줄 · 수백 쪽)는 한 칸씩 못 넘김 — 처음·끝 단추와 쪽 번호 칸을 둠(2026-09-15 브레인).
const pagerButton = (key: Parameters<typeof L>[0], onClick: () => void) =>
createButton({ label: L(key), variant: "ghost", onClick });
const first = pagerButton("Z01_MasterData_First", () => goTo(1));
const prev = pagerButton("Z01_MasterData_Prev", () => goTo(state.page - 1));
const next = pagerButton("Z01_MasterData_Next", () => goTo(state.page + 1));
const end = pagerButton("Z01_MasterData_Last", () => goTo(Infinity));
const toolbar = el("div", {
className: "z01-master__toolbar",
children: [search.root, hiddenLabel],
});
const pager = el("div", {
className: "z01-master__pager",
children: [first, prev, pageInput, pageInfo, next, end],
});
const root = el("section", {
className: "z01-master__panel",
children: [el("div", { children: [title, titleKey] }), toolbar, grid, pager],
});
toolbar.hidden = true;
pager.hidden = true;
function goTo(page: number): void {
const pages = pageCount(last?.total ?? 0, PAGE_SIZE);
state.page = page === Infinity ? pages : clampPage(page, pages);
void load();
}
async function load(): Promise<void> {
if (!state.file || !state.table) return;
grid.classList.add("is-loading");
let data: MasterRows | undefined;
try {
data = await fetchLatest({
file: state.file.id,
table: state.table.id,
page: state.page,
size: PAGE_SIZE,
q: state.q,
});
} catch (error) {
grid.classList.remove("is-loading");
showToast(error instanceof Error ? error.message : L("Z01_MasterData_LoadFailed"), "error");
return;
}
// 옛 요청 — 로딩 표시는 뒤 요청 몫이라 그대로 둠.
if (!data) return;
grid.classList.remove("is-loading");
last = data;
draw();
}
function draw(): void {
if (!last) return;
const columns = shownColumns(last.columns, showHidden);
const headRow = el("tr");
for (const column of columns) {
const th = el("th", { children: [el("span", { text: columnTitle(column) })] });
if (column.label !== column.key) {
th.append(el("span", { className: "z01-master__key", text: column.key }));
}
headRow.append(th);
}
const body = el("tbody");
for (const row of last.rows) {
const tr = el("tr");
for (const text of rowCells(row, columns)) {
tr.append(el("td", { text, attrs: { title: text } }));
}
body.append(tr);
}
grid.replaceChildren(
last.rows.length
? el("table", {
className: "z01-master__grid",
children: [el("thead", { children: [headRow] }), body],
})
: el("p", { className: "z01-master__note", text: L("Z01_MasterData_NoRows") }),
);
const pages = pageCount(last.total, PAGE_SIZE);
pageInput.value = String(state.page);
pageInput.max = String(pages);
pageInfo.textContent = `/ ${pages.toLocaleString("ko-KR")} ${L("Z01_MasterData_Page")} · ${last.total.toLocaleString("ko-KR")} ${L("Z01_MasterData_Rows")}`;
first.disabled = prev.disabled = state.page <= 1;
next.disabled = end.disabled = state.page >= pages;
}
function pick(file: MasterFile, table: MasterTable): void {
state.file = file;
state.table = table;
state.page = 1;
title.textContent = `${file.label} ${table.label}`;
titleKey.textContent = `${file.key} / ${table.key}`;
toolbar.hidden = false;
pager.hidden = false;
last = null;
grid.replaceChildren();
void load();
}
return { root, pick };
}