/* ============================================================================= * M01_MasterData_UI_Rows.ts * 요소 파일(인력·재료·기계·환율·요율) 표 — 한 줄 = 요소 · 이름 찾기 · 쪽 나눔 · 눌러 고침(노랑) * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch"; import { addRow, editRow, onDraftChange, peek, removeAdd, same, setAdd, toggleDelete, } from "./M01_MasterData_Draft"; import { buildCell, buildPager, coerce, flatten, isScalar, show, withCell, } from "./M01_MasterData_UI_Cells"; const SIZE = 50; const FALLBACK: Row = { 열쇠: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" }; /** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */ function blank(sample: Row): Row { const out: Row = {}; for (const [k, v] of Object.entries(sample)) { if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row); else out[k] = typeof v === "string" && k !== "값" ? "" : null; } return out; } /** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */ export function renderRows(host: HTMLElement, file: string, q: string): () => void { let page = 1; let data: RowsPage | null = null; const paint = (): void => { if (!data) return; const d = data; const draft = peek(file); const adds = draft?.adds ?? []; const cols: string[] = []; for (const row of [...adds, ...d.rows]) { for (const k of Object.keys(flatten(row))) if (!cols.includes(k)) cols.push(k); } const head = el("tr", { children: [el("th"), ...cols.map((c) => el("th", { text: c }))] }); const body = el("tbody"); adds.forEach((row, i) => { const flat = flatten(row); const tr = el("tr", { className: "is-new" }); tr.append(actionCell(L("M01_RowRemove"), () => removeAdd(file, i))); for (const c of cols) { tr.append( buildCell(show(flat[c]), { changed: true, onEdit: (text) => setAdd(file, i, withCell(row, c, coerce(text, flat[c]))), }), ); } body.append(tr); }); for (const row of d.rows) { const key = String(row["열쇠"]); const cur = draft?.edits[key] ?? row; const deleted = draft?.deletes.includes(key) ?? false; const orig = flatten(row); const now = flatten(cur); const tr = el("tr"); tr.classList.toggle("is-deleted", deleted); tr.append( actionCell(deleted ? L("M01_RowRestore") : L("M01_RowDelete"), () => toggleDelete(file, d.version, key), ), ); for (const c of cols) { const editable = !deleted && isScalar(orig[c]); tr.append( buildCell(show(now[c]), { changed: !same(now[c], orig[c]), onEdit: editable ? (text) => editRow(file, d.version, key, row, withCell(cur, c, coerce(text, orig[c]))) : undefined, }), ); } body.append(tr); } const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" }); add.addEventListener("click", () => addRow(file, d.version, blank(d.rows[0] ?? FALLBACK))); const empty = d.rows.length || adds.length ? [] : [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })]; host.replaceChildren( el("div", { className: "m01-master__bar", children: [add, buildPager(d.total, SIZE, d.page, go)], }), el("div", { className: "m01-master__grid-wrap", children: [ el("table", { className: "m01-master__grid", children: [el("thead", { children: [head] }), body], }), ], }), ...empty, ); }; const go = (next: number): void => { page = next; void load(); }; const load = async (): Promise => { try { data = await fetchRows(file, page, SIZE, q); paint(); } catch (error) { showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); } }; const off = onDraftChange(paint); void load(); return off; } function actionCell(label: string, onClick: () => void): HTMLTableCellElement { const button = el("button", { className: "m01-master__row-btn", text: label, attrs: { type: "button" }, }); button.addEventListener("click", onClick); return el("td", { children: [button] }); }