/* ============================================================================= * M01_MasterData_UI_Machine.ts * M01 기계 목록(PLAN 821-2 · 요구 790-2) — 표는 목록용 열만(이름 · 규격 · 구분 · 가격 · 손료계수 · * 주연료 · 조종원 · 시간당 경비(서버 값) · 별칭 · 비고) · 줄을 누르면 상세 페이지 · [새 기계] = 양식. * 단위 · 출처 열 없음 · 고친 것 · 새 줄 · 지울 줄은 초안(노랑 · 새 · 지움 표시) — 저장은 페이지 [저장]. * 「사용」 열(842-2) = 스위치 · 끄면 줄 맨 위 칸 `비활성: true`(초안 → [저장]) · 끈 줄은 흐림 · * 머리 「비활성 같이 보기」 끄면 서버가 끈 줄을 뺌(`rows` state 「켜짐」). * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createEmpty } from "@ui/ui_template_empty"; import { createToggle } from "@ui/ui_template_toggle"; import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager"; import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch"; import { editRow, onDraftChange, peek, same } from "./M01_MasterData_Draft"; import { fetchMachineHourly } from "./M01_MasterData_Machine_Api"; import { buildPager } from "./M01_MasterData_UI_Cells"; import { saveView, type Pick } from "./M01_MasterData_UI_Side"; const BAR_H = 52; const num = (v: unknown): string => typeof v === "number" ? v.toLocaleString("ko-KR", { maximumFractionDigits: 4 }) : ""; /** 목록 열 — 이름 · 줄 → 글 */ const COLS: [string, (r: Row) => string][] = [ ["이름", (r) => String(r["이름"] ?? "")], ["규격", (r) => String(r["규격"] ?? "")], ["구분", (r) => [r["구분"], r["상세구분"]].filter(Boolean).join(" · ")], ["가격(천원)", (r) => num(val(r)["가격"])], ["손료계수", (r) => num(val(r)["손료계수"])], [ "주연료", (r) => val(r)["주연료"] ? `${val(r)["주연료종류"] ?? ""} ${num(val(r)["주연료"])}ℓ`.trim() : "", ], [ "조종원", (r) => val(r)["조종원"] ? `${num(val(r)["조종원"])}인 ${val(r)["조종원직종"] ?? ""}`.trim() : "", ], ]; const val = (r: Row): Record => (r["값"] ?? {}) as Record; const OFF = "비활성"; // 842-2 기계 줄 맨 위 칸 — true 만 · 켜짐 = 칸 없음 let showOff = true; // 「비활성 같이 보기」(페이지 안에서 기억) /** 돌려받은 함수 = 걷기. `open(target)` = 상세(키 · 새 줄 `add:<차례>` · 새 기계 `new`). */ export function renderMachines( host: HTMLElement, pick: Pick, q: string, open: (target: string) => void, ): () => void { const file = pick.file.file; let size = guessRows(host, BAR_H); let page = pick.view?.page ?? 1; let data: RowsPage | null = null; const hourly = new Map(); const hourText = (key: string): string => hourly.get(key) ?? "…"; /** 「사용」 칸 — 스위치(새 줄은 빈 칸) · 바꾸면 초안 */ const useCell = (row: Row, cur: Row, key: string): HTMLElement => { const td = el("td", { className: "m01-machine__use" }); if (!key || !data) return td; const version = data.version; const sw = createToggle({ label: "", size: 20, checked: !cur[OFF], onChange: (on) => { const next: Row = { ...cur }; if (on) delete next[OFF]; else next[OFF] = true; editRow(file, version, key, row, next); }, }); sw.root.setAttribute("aria-label", L("M01_MachineUse")); sw.root.addEventListener("click", (e) => e.stopPropagation()); td.classList.toggle("is-changed", !!cur[OFF] !== !!row[OFF]); td.append(sw.root); return td; }; const line = (row: Row, cur: Row, cls: string, target: string, key = ""): HTMLElement => { const tr = el("tr", { className: `m01-machine__row ${cls} ${cur[OFF] ? "is-off" : ""}`.trim(), attrs: { "data-key": key || target }, children: [ ...COLS.map(([, get]) => { const text = get(cur); const td = el("td", { text, attrs: { title: text } }); td.classList.toggle("is-changed", cls !== "is-new" && text !== get(row)); return td; }), el("td", { className: "m01-machine__money", text: key ? hourText(key) : "", attrs: key ? { "data-hourly": key } : {}, }), el("td", { text: String(cur["별칭"] ?? ""), attrs: { title: String(cur["별칭"] ?? "") } }), el("td", { text: String(cur["비고"] ?? ""), attrs: { title: String(cur["비고"] ?? "") } }), useCell(row, cur, key), ], }); tr.addEventListener("click", () => open(target)); return tr; }; const paint = (): void => { if (!data) return; const d = data; const draft = peek(file); const body = el("tbody"); (draft?.adds ?? []).forEach((r, i) => body.append(line(r, r, "is-new", `add:${i}`))); for (const row of d.rows) { const key = String(row["키"]); const cur = draft?.edits[key] ?? row; const cls = draft?.deletes.includes(key) ? "is-deleted" : same(cur, row) ? "" : "is-changed"; body.append(line(row, cur, cls, key, key)); } const add = createButton({ label: L("M01_MachineNew"), variant: "ghost", iconStart: "plus" }); add.addEventListener("click", () => open("new")); const offSwitch = createToggle({ label: L("M01_MachineShowOff"), checked: showOff, onChange: (on) => { showOff = on; page = 1; void load(); }, }); const empty = d.rows.length || draft?.adds.length ? [] : [createEmpty({ text: L("M01_NoRows"), small: true })]; host.replaceChildren( el("div", { className: "ui-toolbar m01-master__bar", children: [add, offSwitch.root] }), el("div", { className: "ui-table-wrap m01-master__grid-wrap m01-machine__wrap", children: [ el("table", { className: "ui-table m01-master__grid m01-machine__grid", children: [ el("thead", { children: [ el("tr", { children: [ ...COLS.map(([h]) => el("th", { text: h })), el("th", { text: `${L("M01_MachineHourly")}(원)` }), el("th", { text: "별칭" }), el("th", { text: "비고" }), el("th", { className: "m01-machine__use", text: L("M01_MachineUse") }), ], }), ], }), body, ], }), ], }), buildPager(d.total, size, d.page, go), ...empty, ); fit.ask(); }; /** 시간당 경비 — 표를 그린 뒤 서버 값으로 채움(마스터 판마다 서버가 한 번 셈) */ const fillHourly = async (keys: string[]): Promise => { const want = keys.filter((k) => !hourly.has(k)); if (!want.length) return; try { const got = await fetchMachineHourly(want); for (const [k, h] of Object.entries(got)) hourly.set(k, h.계 === undefined ? (h.reason ?? "—") : num(h.계)); } catch { for (const k of want) hourly.set(k, "—"); } host.querySelectorAll("td[data-hourly]").forEach((td) => { const text = hourly.get(td.dataset.hourly!); if (text) td.textContent = text; }); }; const go = (next: number): void => { page = next; void load(); }; let ticket = 0; // 늦게 온 옛 응답이 새 목록을 덮지 않게(829-1) const load = async (): Promise => { const mine = ++ticket; try { const state = showOff ? pick.state : "켜짐"; const got = await fetchRows(file, page, size, q, false, pick.sub, pick.detail, state); if (mine !== ticket) return; data = got; saveView(pick.group, { page: data.page }); paint(); void fillHourly(data.rows.map((r) => String(r["키"]))); } catch (error) { showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); } }; const fit = watchFit( host, () => host.querySelector(".m01-master__grid-wrap"), (rows) => { if (rows === size) return; page = pageKeepFirst(data?.page ?? page, size, rows); size = rows; void load(); }, ); const off = onDraftChange(paint); void load(); return () => { ticket++; fit.stop(); off(); }; }