/* ============================================================================= * M01_MasterData_UI_Logic_Calc.ts * 시험 계산 — 받을 값을 넣고 [계산] → 줄별 금액 · 비목 합 · 멈춘 까닭(엔진 글 그대로) * 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀 * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { runCalc, type CalcAnswer, type CalcLine, type LogicRow, } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { tx } from "./M01_MasterData_UI_Logic_Text"; export interface CalcContext { book: string; /** 저장된 열쇠 — 새 로직은 null */ savedKey: string | null; file: string; row: LogicRow; dirty: () => boolean; /** 로직마다 넣은 값 — 다시 그려도 남음 */ values: Record; onLines: (lines: CalcLine[] | null) => void; } const SUMS = ["노무비", "재료비", "경비", "계"]; export function buildCalc(host: HTMLElement, ctx: CalcContext): void { const fields = (ctx.row.입력 ?? []).map((spec) => { const name = spec.이름; let control: HTMLInputElement | HTMLSelectElement; if (spec.고르기?.length) { control = el("select", { className: "m01-logic__input" }); for (const option of ["", ...spec.고르기.map(String)]) { control.append(el("option", { text: option, attrs: { value: option } })); } } else { control = el("input", { className: "m01-logic__input", attrs: { type: "text", inputmode: "decimal" }, }); if (spec.범위) control.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`; } control.value = ctx.values[name] ?? ""; control.addEventListener("input", () => (ctx.values[name] = control.value)); control.addEventListener("change", () => (ctx.values[name] = control.value)); const label = spec.단위 ? `${name} (${spec.단위})` : name; return el("label", { className: "m01-logic__field", children: [el("span", { text: label }), control], }); }); const out = el("div", { className: "m01-logic__calc-out" }); const run = async (): Promise => { const inputs: Record = {}; for (const spec of ctx.row.입력 ?? []) { const raw = (ctx.values[spec.이름] ?? "").trim(); if (raw === "") continue; // 빈 칸은 안 보냄 — 엔진이 「입력 없음」 으로 멈춤 // 고르기는 원래 값(수면 수) 그대로 — 글 "35" 로 보내면 「고르기 밖」 const option = spec.고르기?.find((o) => String(o) === raw); inputs[spec.이름] = option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw); } const draft = ctx.dirty() || ctx.savedKey === null; try { const answer = await runCalc({ book: ctx.book, key: ctx.savedKey ?? ctx.row.열쇠, inputs, ...(draft ? { row: ctx.row, file: ctx.file } : {}), }); ctx.onLines(answer.ok ? (answer.lines ?? null) : null); out.replaceChildren(...answerView(answer, draft)); } catch (error) { showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); } }; host.replaceChildren( el("h3", { text: tx("Calc_Title") }), ...(fields.length ? fields : [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]), createButton({ label: tx("Calc_Run"), onClick: () => void run() }), out, ); } function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] { const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : []; if (!answer.ok) { return [ ...note, el("div", { className: "m01-logic__reasons", children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: answer.reason })], }), ]; } const parts: HTMLElement[] = [...note]; if (answer.lines) { const rows = answer.lines.map((line) => el("tr", { children: [ el("td", { text: line.출처 ? `${line.이름} (${line.출처})` : line.이름 }), el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }), el("td", { className: "m01-logic__money", text: formatNumber(line.금액) }), ], }), ); parts.push( el("table", { className: "m01-logic__grid", children: [ el("thead", { children: [ el("tr", { children: [tx("Head_Name"), tx("Ho_Qty"), tx("Ho_Amount")].map((h) => el("th", { text: h }), ), }), ], }), el("tbody", { children: rows }), ], }), ); } if (answer.sums) { parts.push( el("dl", { className: "m01-logic__sums", children: SUMS.flatMap((k) => [ el("dt", { text: k === "계" ? tx("Calc_Sum") : k }), el("dd", { className: "m01-logic__money", text: formatNumber(answer.sums?.[k] ?? 0) }), ]), }), ); } if (answer.result !== undefined) { parts.push( el("dl", { className: "m01-logic__sums", children: [ el("dt", { text: tx("Calc_Result") }), el("dd", { text: formatNumber(answer.result) }), ], }), ); } const middle = Object.entries(answer.middle ?? {}); if (middle.length) { parts.push( el("h4", { text: tx("Middle_Title") }), el("dl", { className: "m01-logic__sums", children: middle.flatMap(([k, v]) => [ el("dt", { text: k }), el("dd", { text: formatNumber(v) }), ]), }), ); } return parts; }