/* ============================================================================= * M02_DF_Summary.ts * 단가산출 다이어그램 — 원가 합계 칸: 큰 합계 「1㎡ 당 ○원」 · 노무비 · 재료비 · 경비 · 출력 상자별 줄 * · 값(비용 아님) 출력 · 빈 받는 자리 · 오류 · 예시값(변수 상자의 예시값을 여기서도 바꿈) * 값은 화면 계산(Eval `evalLogic`) 결과를 받음 — 이 파일은 셈하지 않고 읽어서 보여 줌. * 모양은 docs/raw/M01_쉬운단가/원가다이어그램_정함.md 점검 5 · 6. 이 폴더째 지워도 됨. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createSelectField, el } from "@ui/ui_template_elements"; import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money"; import { checkLogic, type CfEvalResult, type CfValues } from "./M02_DF_Eval"; import type { CfBox, CfLogic, CfVariable } from "./M02_DF_Model"; import { showValue } from "./M02_DF_OutputPanel"; import "./M02_DF_Summary.css"; const TEXT = { Per: ["{per} 당", "per {per}"], Total: ["원가 합계", "Cost total"], Won: ["원", "KRW"], Labor: ["노무비", "Labor cost"], Material: ["재료비", "Material cost"], Expense: ["경비", "Expense"], Lines: ["출력 상자별", "By output box"], HName: ["이름", "Name"], HCost: ["비용 종류", "Cost kind"], HValue: ["값", "Value"], HUnit: ["단위", "Unit"], Plain: ["값(비용 아님)", "Values (not costs)"], Sample: ["예시값", "Sample values"], SampleHint: [ "여기서 바꾸면 변수 상자의 예시값도 바뀌어요", "Changing here changes the variable box too", ], Problems: ["고칠 것", "To fix"], NoLink: ["「{box}」 출력 상자에 이어진 선이 없어요", "Output box “{box}” has no line yet"], NoOutput: ["출력 상자가 아직 없어요", "There is no output box yet"], Bad: ["숫자를 넣어 주세요", "Enter a number"], Range: ["{a} ∼ {b} 사이", "between {a} and {b}"], Pick: ["고르세요", "Choose"], Cant: ["지금은 바꿀 수 없어요", "Cannot change this now"], Plain3: ["값", "Value"], } as const; type TextKey = keyof typeof TEXT; const ts = (key: TextKey, fill: Record = {}): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace( /\{(\w+)\}/g, (m, k: string) => fill[k] ?? m, ); const COSTS = ["노무비", "재료비", "경비"] as const; const COST_KEY = { 노무비: "Labor", 재료비: "Material", 경비: "Expense" } as const; const costText = (c: string): string => (c === "값" ? ts("Plain3") : c); /** 빈 받는 자리 · 출력 상자 없음 — 쉬운 말 */ function emptyPlaces(lg: CfLogic): string[] { const outs = lg.boxes.filter((b) => b.kind === "output"); if (!outs.length) return [ts("NoOutput")]; return outs .filter((o) => !lg.links.some((l) => l.to.box === o.id)) .map((o) => ts("NoLink", { box: o.name })); } /** 변수 상자의 예시값 칸 하나 */ function sampleField( v: CfVariable, put: (value: number | string) => void, msg: HTMLElement, ): HTMLElement { const label = el("span", { text: v.unit && v.unit !== "-" ? `${v.name} (${v.unit})` : v.name }); let control: HTMLElement; if (v.type === "choice" && v.choices?.length) { control = createSelectField({ options: [ { value: "", text: ts("Pick") }, ...v.choices.map((c) => ({ value: String(c), text: String(c) })), ], value: v.example === null ? "" : String(v.example), compact: true, ariaLabel: v.name, onChange: (s) => { const hit = v.choices?.find((c) => String(c) === s); if (hit !== undefined) put(hit); }, }).root; } else { const box = el("input", { className: "m02-dfsum__input", attrs: { type: "text", inputmode: v.type === "number" ? "decimal" : "text", "aria-label": v.name, }, }); if (v.range) box.placeholder = ts("Range", { a: String(v.range[0]), b: String(v.range[1]) }); box.value = v.example === null ? "" : String(v.example); box.addEventListener("change", () => { const raw = box.value.trim(); if (raw === "") return; if (v.type !== "number") return put(raw); const n = Number(raw); if (Number.isNaN(n)) msg.textContent = ts("Bad"); else put(n); }); control = box; } return el("label", { className: "m02-dfsum__field", children: [label, control] }); } /** 원가 합계 칸 — result = 화면 계산 결과 · on 이 있으면 예시값을 바꿀 수 있음 · opts.values = 받은 입력 값(검사가 씀) */ export function renderSummary( host: HTMLElement, lg: CfLogic, result: CfEvalResult, on?: { change(lg: CfLogic): void }, opts?: { values?: CfValues }, ): void { const money = !lg.resultUnit || lg.resultUnit.startsWith("원"); const perUnit = lg.resultUnit.split("/").slice(1).join("/"); const perText = !perUnit ? ts("Total") : ts("Per", { per: /^\d/.test(perUnit) ? perUnit : `1${perUnit}` }); const { cost } = result; const big = el("p", { className: "m02-dfsum__big", children: [ el("span", { text: money ? perText : ts("Total") }), el("strong", { text: `${formatMoney(cost.계)} ${ts("Won")}` }), ], }); const costs = el("dl", { className: "m02-dfsum__costs", children: COSTS.flatMap((c) => [ el("dt", { text: ts(COST_KEY[c]) }), el("dd", { text: formatMoney(cost[c]) }), ]), }); /* 출력 상자별 줄 — 비용 줄 · 값(비용 아님)은 따로 */ const outs: CfBox[] = lg.boxes.filter((b) => b.kind === "output"); const costed = outs.filter((b) => b.output?.cost !== "값"); const plain = outs.filter((b) => b.output?.cost === "값"); const line = (b: CfBox, isCost: boolean): HTMLElement => el("tr", { children: [ el("td", { text: b.name }), ...(isCost ? [el("td", { text: costText(b.output?.cost ?? "") })] : []), el("td", { className: "m02-dfsum__num", text: showValue(result.outputs[b.id], isCost), }), el("td", { text: b.output?.unit ?? b.unit }), ], }); const table = (list: CfBox[], isCost: boolean, title: string): HTMLElement => el("div", { className: "m02-dfsum__block", children: [ el("strong", { text: title }), el("div", { className: "m02-dfsum__scroll", children: [ el("table", { className: "m02-dfsum__table", children: [ el("thead", { children: [ el("tr", { children: [ el("th", { text: ts("HName") }), ...(isCost ? [el("th", { text: ts("HCost") })] : []), el("th", { text: ts("HValue") }), el("th", { text: ts("HUnit") }), ], }), ], }), el("tbody", { children: list.map((b) => line(b, isCost)) }), ], }), ], }), ], }); /* 고칠 것 — 빈 받는 자리 · 검사 · 계산 오류 */ let checked: string[] = []; try { checked = checkLogic(lg, opts?.values ?? {}); } catch { checked = []; } const problems = [...new Set([...emptyPlaces(lg), ...checked, ...result.errors])]; /* 예시값 — 변수 상자의 변수마다 */ const msg = el("p", { className: "m02-dfsum__msg" }); const fields = lg.boxes .filter((b) => b.kind === "input" && b.input?.mode === "variable") .flatMap((b) => (b.input?.variables ?? []).map((v, i) => sampleField( v, (value) => { msg.textContent = ""; if (!on) return; try { on.change({ ...lg, boxes: lg.boxes.map((x) => x.id === b.id && x.input ? { ...x, input: { ...x.input, variables: x.input.variables.map((y, j) => j === i ? { ...y, example: value } : y, ), }, } : x, ), }); } catch { msg.textContent = ts("Cant"); } }, msg, ), ), ); const plainValues = Object.entries(cost.값들); host.replaceChildren( big, costs, ...(costed.length ? [table(costed, true, ts("Lines"))] : []), ...(plain.length ? [table(plain, false, ts("Plain"))] : plainValues.length ? [ el("div", { className: "m02-dfsum__block", children: [ el("strong", { text: ts("Plain") }), ...plainValues.map(([k, v]) => el("p", { className: "m02-dfsum__muted", text: `${k} · ${showValue(v)}` }), ), ], }), ] : []), ...(fields.length ? [ el("div", { className: "m02-dfsum__strip", children: [ el("strong", { text: ts("Sample") }), el("div", { className: "m02-dfsum__fields", children: fields }), el("p", { className: "m02-dfsum__muted", text: ts("SampleHint") }), msg, ], }), ] : []), ...(problems.length ? [ el("div", { className: "m02-dfsum__problems", children: [ el("strong", { text: ts("Problems") }), el("ul", { children: problems.map((t) => el("li", { text: t })) }), ], }), ] : []), ); }