/* ============================================================================= * M02_CF_OutputPanel.ts * 단가산출 다이어그램 — 출력 상자 판. 출력 상자를 고르면 아래에 뜸 * 받은 값(어디서 왔나 · 값 · 단위) · 비용 종류 고르기(노무비 · 재료비 · 경비 · 값(비용 아님)) · 단위 · 이름 * 모양은 docs/raw/M01_쉬운단가/원가다이어그램_정함.md 「상자 · 선 약속」 · Model 타입 그대로. 이 폴더째 지워도 됨. * ========================================================================== */ 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 { evalLogic, type CfEvalResult, type CfValue } from "./M02_CF_Eval"; import { portsOut, type CfBox, type CfCost, type CfLogic } from "./M02_CF_Model"; import "./M02_CF_OutputPanel.css"; const TEXT = { Title: ["출력", "Output"], Name: ["이름", "Name"], Cost: ["비용 종류", "Cost kind"], Labor: ["노무비", "Labor cost"], Material: ["재료비", "Material cost"], Expense: ["경비", "Expense"], Plain: ["값(비용 아님)", "Value (not a cost)"], Unit: ["단위", "Unit"], UnitHint: ["예: 원 · ㎥ · 시간 · -", "e.g. KRW · ㎥ · h · -"], Got: ["받은 값", "Received value"], From: ["어디서", "From"], Value: ["값", "Value"], NoLink: [ "아직 이어진 선이 없어요 — 입력이나 함수 상자에서 이 상자로 선을 이어 주세요", "Nothing is linked yet — draw a line from an input or function box", ], Table: ["표", "Table"], Calc: ["계산 중이 아니에요", "Not calculated"], Cant: ["지금은 바꿀 수 없어요", "Cannot change this now"], PlainHint: [ "원가 합계에 안 넣고 값으로만 보여요", "Shown as a value, not added to the cost total", ], } as const; type TextKey = keyof typeof TEXT; const to = (key: TextKey): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0]; const COSTS: [CfCost, TextKey][] = [ ["노무비", "Labor"], ["재료비", "Material"], ["경비", "Expense"], ["값", "Plain"], ]; /** 값 한 칸의 글 — 수 · 글 · 참거짓 · 표 */ export function showValue(v: CfValue | undefined, money = false): string { if (v === undefined || v === null) return "—"; if (Array.isArray(v)) return to("Table"); if (typeof v === "number") return money ? formatMoney(v) : v.toLocaleString("ko-KR", { maximumFractionDigits: 6 }); return String(v); } const withBox = (lg: CfLogic, id: string, edit: (b: CfBox) => CfBox): CfLogic => ({ ...lg, boxes: lg.boxes.map((b) => (b.id === id ? edit(b) : b)), }); /** 출력 상자 판 — `opts.result` 가 있으면 그 값으로 「받은 값」을 보임(없으면 지금 계산을 한 번 해 봄) */ export function renderOutputPanel( host: HTMLElement, lg: CfLogic, boxId: string, on: { change(lg: CfLogic): void }, opts: { result?: CfEvalResult } = {}, ): void { const box = lg.boxes.find((b) => b.id === boxId); if (!box || box.kind !== "output") { host.replaceChildren(); return; } const msg = el("p", { className: "m01-cfout__msg" }); const emit = (make: () => CfLogic): void => { msg.textContent = ""; try { on.change(make()); } catch { msg.textContent = to("Cant"); } }; const cost = box.output?.cost ?? "노무비"; /* 받은 값 — 이 상자로 들어오는 선 하나(받는 자리는 하나) */ const link = lg.links.find((l) => l.to.box === box.id); const from = link && lg.boxes.find((b) => b.id === link.from.box); let result = opts.result; if (!result && link) { try { result = evalLogic(lg); } catch { result = undefined; } } let unit = ""; if (from && link) { try { unit = portsOut(from).find((p) => p.name === link.from.name)?.unit ?? from.unit; } catch { unit = from.unit; } } const got = el("div", { className: "m01-cfout__got" }); if (link && from) { const value = result?.ports[from.id]?.[link.from.name] ?? result?.outputs[box.id]; got.replaceChildren( el("strong", { text: to("Got") }), el("dl", { children: [ el("dt", { text: to("From") }), el("dd", { text: `${from.name} · ${link.from.name}` }), el("dt", { text: to("Value") }), el("dd", { className: "m01-cfout__num", text: `${showValue(value, cost !== "값")}${unit && unit !== "-" ? ` ${unit}` : ""}`, }), ], }), ); } else { got.replaceChildren( el("strong", { text: to("Got") }), el("p", { className: "m01-cfout__warn", text: to("NoLink") }), ); } /* 이름 · 비용 종류 · 단위 */ const nameBox = el("input", { className: "m01-cfout__input", attrs: { type: "text", "aria-label": to("Name") }, }); nameBox.value = box.name; nameBox.addEventListener("change", () => { const next = nameBox.value.trim(); if (!next || next === box.name) { nameBox.value = box.name; return; } emit(() => withBox(lg, box.id, (b) => ({ ...b, name: next }))); }); const costField = createSelectField({ label: to("Cost"), options: COSTS.map(([value, key]) => ({ value, text: to(key) })), value: cost, compact: true, onChange: (v) => emit(() => withBox(lg, box.id, (b) => ({ ...b, output: { unit: b.output?.unit ?? b.unit, cost: v as CfCost }, })), ), }); const unitBox = el("input", { className: "m01-cfout__input", attrs: { type: "text", placeholder: to("UnitHint"), "aria-label": to("Unit") }, }); unitBox.value = box.output?.unit ?? box.unit; unitBox.addEventListener("change", () => { const next = unitBox.value.trim() || "-"; emit(() => withBox(lg, box.id, (b) => ({ ...b, unit: next, output: { cost: b.output?.cost ?? "노무비", unit: next }, })), ); }); const field = (label: string, control: HTMLElement): HTMLElement => el("label", { className: "m01-cfout__field", children: [el("span", { text: label }), control], }); host.replaceChildren( el("strong", { className: "m01-cfout__title", text: `${to("Title")} · ${box.name}` }), el("div", { className: "m01-cfout__body", children: [ got, el("div", { className: "m01-cfout__fields", children: [field(to("Name"), nameBox), costField.root, field(to("Unit"), unitBox)], }), ], }), ...(cost === "값" ? [el("p", { className: "m01-cfout__muted", text: to("PlainHint") })] : []), msg, ); }