/* ============================================================================= * M01_MasterData_UI_Tables.ts * 표형 그룹(소요량·계수) — 구분·상세구분·찾기로 거른 표 목록(파일을 가로지름 · 쪽 나눔) * → 표 하나(조건 칸 + 값 칸의 긴 줄) · 눌러 고침(노랑) · 줄 더하기·지우기 * ========================================================================== */ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { fetchTable, fetchTables, type Row, type TableHead, type TableUsage, } from "./M01_MasterData_Api_Fetch"; import { saveView, type Pick } from "./M01_MasterData_UI_Side"; import { onDraftChange, peek, same, setTable, tableItems, type TableItem, } from "./M01_MasterData_Draft"; import { buildCell, buildPager, coerce, show, toNumberish, toRange, } from "./M01_MasterData_UI_Cells"; const LIST_SIZE = 30; const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "비고"] as const; /** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */ const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"]; /** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. `openLogic` = 용도의 로직키 눌렀을 때 로직 탭으로. */ export function renderTables( host: HTMLElement, pick: Pick, q: string, openLogic: (key: string) => void, ): () => void { let stop = (): void => {}; let usage = pick.view?.usage ?? ""; const list = (page = pick.view?.page ?? 1): void => { stop(); stop = (): void => {}; void showList( host, pick, q, usage, page, (file, key) => { stop = openTable(host, file, key, () => list(page), openLogic); }, (next) => { usage = next; saveView(pick.group, { usage, page: 1 }); list(1); }, ); }; list(); const off = onDraftChange(() => host.querySelector(".m01-master__tables") && markChanged(host)); return () => { stop(); off(); }; } /** 목록 위 「고침」 표시만 다시 — 목록을 다시 받지 않음. */ function markChanged(host: HTMLElement): void { host.querySelectorAll(".m01-master__table-row").forEach((row) => { const changed = peek(row.dataset.file!)?.tables ?? {}; row.classList.toggle("is-changed", row.dataset.key! in changed); }); } async function showList( host: HTMLElement, pick: Pick, q: string, usage: string, page: number, open: (file: string, key: string) => void, onUsageChange: (usage: string) => void, ): Promise { let got: { total: number; tables: TableHead[] }; try { got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, LIST_SIZE, usage); } catch (error) { showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); return; } saveView(pick.group, { page, usage }); const rows = got.tables.map((t) => { const tr = el("tr", { className: "m01-master__table-row", attrs: { "data-key": t.키, "data-file": t.file }, children: LIST_COLS.map((c) => el("td", { text: String(t[c] ?? ""), attrs: { title: String(t[c] ?? "") } }), ), }); tr.classList.toggle("is-changed", t.키 in (peek(t.file)?.tables ?? {})); tr.addEventListener("click", () => open(t.file, t.키)); return tr; }); const usageField = createSelectField({ options: [ { value: "", text: L("M01_All") }, ...USAGE_TARGETS.map((v) => ({ value: v, text: v })), ], value: usage, compact: true, label: L("M01_UsageTarget"), }); usageField.select.addEventListener("change", () => onUsageChange(usageField.select.value)); host.replaceChildren( el("div", { className: "m01-master__filter", children: [usageField.root] }), buildPager( got.total, LIST_SIZE, page, (p) => void showList(host, pick, q, usage, p, open, onUsageChange), ), el("div", { className: "m01-master__grid-wrap m01-master__tables", children: [ el("table", { className: "m01-master__grid", children: [ el("thead", { children: [el("tr", { children: LIST_COLS.map((c) => el("th", { text: c })) })], }), el("tbody", { children: rows }), ], }), ], }), ); } /** 표 하나. 돌려받은 함수 = 해제. */ function openTable( host: HTMLElement, file: string, key: string, back: () => void, openLogic: (key: string) => void, ): () => void { let off = (): void => {}; void (async () => { let version: string; let table: Row & { 줄: Row[] }; try { ({ version, table } = await fetchTable(file, key)); } catch (error) { showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); return; } const { 줄: orig, ...base } = table; const conds = (table.조건 ?? {}) as Record; const values = (table.값칸 ?? {}) as Record; const cols = [...Object.keys(conds), ...Object.keys(values)]; for (const row of orig) for (const k of Object.keys(row)) if (!cols.includes(k)) cols.push(k); const items = (): TableItem[] => tableItems(file, key) ?? orig.map((r, o) => ({ o, r })); const commit = (next: TableItem[]): void => setTable(file, version, key, base, next, orig); const parse = (col: string, text: string, prev: unknown): unknown => { if (conds[col] === "범위") return toRange(text); if (conds[col] === "고르기") return text; return col in values || conds[col] === "수" ? toNumberish(text) : coerce(text, prev); }; const headText = (c: string): string => c in conds ? `${c} (${conds[c]})` : c in values ? `${c} (${values[c]})` : c; const paint = (): void => { const now = items(); const body = el("tbody"); now.forEach((item, i) => { const tr = el("tr"); tr.classList.toggle("is-new", item.o === null); const del = el("button", { className: "m01-master__row-btn", text: L("M01_RowRemove"), attrs: { type: "button" }, }); del.addEventListener("click", () => commit(now.filter((_, j) => j !== i))); tr.append(el("td", { children: [del] })); for (const c of cols) { const was = item.o === null ? undefined : orig[item.o][c]; tr.append( buildCell(show(item.r[c]), { changed: item.o === null || !same(item.r[c], was), onEdit: (text) => { const value = parse(c, text, item.r[c]); const r = { ...item.r }; if (value === null || value === "") delete r[c]; // 원문 「-」 = 칸을 뺌 else r[c] = value; commit(now.map((it, j) => (j === i ? { ...it, r } : it))); }, }), ); } body.append(tr); }); const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" }); add.addEventListener("click", () => commit([...now, { o: null, r: {} }])); const backButton = createButton({ label: L("M01_BackToTables"), variant: "ghost" }); backButton.addEventListener("click", () => { off(); back(); }); const notes = ((table.주 ?? []) as string[]).map((n) => el("p", { className: "m01-master__muted", text: n }), ); const usage = table.용도 as TableUsage | undefined; const logicLinks = (usage?.로직키 ?? []).map((k, i) => { const btn = el("button", { className: "m01-master__logic-key", text: k, attrs: { type: "button", title: L("M01_OpenLogic") }, }); btn.addEventListener("click", () => openLogic(k)); return i === 0 ? btn : el("span", { text: ", ", children: [btn] }); }); host.replaceChildren( el("div", { className: "m01-master__bar", children: [ backButton, el("strong", { text: String(table.이름 ?? key) }), el("span", { className: "m01-master__muted", text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`, }), ...(usage ? [ el("span", { className: "m01-master__muted", text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · `, children: logicLinks, }), ] : []), add, ], }), el("div", { className: "m01-master__grid-wrap", children: [ el("table", { className: "m01-master__grid", children: [ el("thead", { children: [ el("tr", { children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))], }), ], }), body, ], }), ], }), ...notes, ); }; off = onDraftChange(paint); paint(); })(); return () => off(); }