/* ============================================================================= * M01_MasterData_UI_LogicLab_FlowView.ts * 흐름 로직의 왼쪽 「지금 내용」 — ② 단가산출 상세(출력 상자를 비용 종류별 줄 · 소계 · 계) · ③ 텍스트 수식(함수 상자마다 * 식 두 줄 — sub2 `formulaLines`). 기본정보 · 시험 계산은 Detail 이 둠. * 값은 DataFlow 계산(Eval · 같은 코드)으로 바로 — 오른쪽 패널이 고치면 LogicLab 이 이 함수를 다시 부름. * 화면은 `buildFlowView(host, lg, values)` 하나. 이 파일은 DataFlow 를 정적으로 가져오므로 LogicLab 은 import() 로 늦게 부름. * 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7 · 8. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { el } from "@ui/ui_template_elements"; import { evalLogic, type CfEvalOptions, type CfEvalResult, type CfValues, } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval"; import { cellValuesOf, formulaLines, type FormulaLine, } from "../M02_MasterTemplete/DataFlow/M02_DF_FormulaView"; import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; import { itemRoundNow, roundText, totalRoundNow, totalRoundOf, } from "../M02_MasterTemplete/DataFlow/M02_DF_Round"; import { flowRows, type FlowFunc, type FlowGroup, type FlowGroupKey, } from "./M01_MasterData_UI_LogicLab_FlowRows"; import "./M01_MasterData_UI_LogicLab_FlowView.css"; const TEXT = { Detail: ["단가산출 상세", "Unit-price detail"], Formula: ["텍스트 수식", "Text formulas"], Labor: ["인력 · 노무비", "Labor · labor cost"], Material: ["재료 · 재료비", "Material · material cost"], Cost: ["경비", "Expense"], Plain: ["값(비용 아님)", "Values (not costs)"], HName: ["이름", "Name"], HUnit: ["단위", "Unit"], HValue: ["값", "Value"], Sub: ["소계", "Subtotal"], Total: ["계", "Total"], TotalNote: ["합계 끝수", "Total rounding"], None: ["출력 상자가 없음", "No output box"], NoFunc: ["함수 상자가 없음", "No function box"], } as const; type TextKey = keyof typeof TEXT; const tf = (key: TextKey): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0]; const GROUP_TITLE: Record = { labor: "Labor", material: "Material", cost: "Cost", plain: "Plain", }; const section = (title: string, body: HTMLElement[]): HTMLElement => el("section", { className: "m01-logic__section m01lab__box", children: [ el("div", { className: "m01-logic__section-head", children: [el("h3", { text: title })] }), ...body, ], }); const groupTable = (g: FlowGroup): HTMLElement => { const sub = g.sub === null ? [] : [ el("tr", { className: "m01lab__sub", children: [ el("td", { text: tf("Sub"), attrs: { colspan: "2" } }), el("td", { className: "m01-logic__money", text: g.subText }), ], }), ]; return el("div", { className: "m01flow__group", children: [ el("h4", { text: tf(GROUP_TITLE[g.key]) }), el("div", { className: "m01-logic__scroll", children: [ el("table", { className: "m01-logic__grid", children: [ el("thead", { children: [ el("tr", { children: [tf("HName"), tf("HUnit"), tf("HValue")].map((h) => el("th", { text: h }), ), }), ], }), el("tbody", { children: [ ...g.lines.map((l) => el("tr", { children: [ el("td", { text: l.name }), el("td", { text: l.unit }), el("td", { className: "m01-logic__money", text: l.text }), ], }), ), ...sub, ], }), ], }), ], }), ], }); }; /** 함수 상자 하나의 글 줄 — 식 보기가 있으면 두 줄(+ 갈래) · 없으면 이름 = 값 */ function funcBlock(f: FlowFunc, lines: FormulaLine[] | null): HTMLElement { const unit = f.unit && f.unit !== "-" ? ` ${f.unit}` : ""; const head = el("p", { className: "m01flow__func", text: `${f.name} = ${f.text}${unit}` }); if (!lines?.length) return el("div", { className: "m01flow__block", children: [head] }); return el("div", { className: "m01flow__block", children: [ head, ...lines.flatMap((l) => [ el("p", { className: "m01lab__expr", text: `${l.name} = ${l.named}` }), el("p", { className: "m01lab__expr", text: `${l.name} = ${l.valued}` }), ...(l.branches ?? []).map((b) => el("p", { className: `m01flow__branch${b.now ? " m01flow__branch--now" : ""}`, text: `${b.now ? "▶ " : ""}${b.when} → ${b.then}`, }), ), ]), ], }); } /** 왼쪽 「지금 내용」 — 단가산출 상세 + 텍스트 수식을 `host` 에 그림(`values` = 입력 값 · 다시 부르면 다시 그림) */ export function buildFlowView( host: HTMLElement, lg: CfLogic, values: CfValues, opts: { logics?: CfEvalOptions["logics"]; result?: CfEvalResult } = {}, ): void { let ev: CfEvalResult; try { ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics }); } catch (e) { host.replaceChildren( section(tf("Detail"), [el("p", { className: "m01-logic__muted", text: String(e) })]), ); return; } const totalRound = totalRoundOf(lg, totalRoundNow()); const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRound }); const detail = section(tf("Detail"), [ ...(rows.groups.length ? rows.groups.map(groupTable) : [el("p", { className: "m01-logic__muted", text: tf("None") })]), el("p", { className: "m01lab__total", attrs: { "data-sum": String(rows.total) }, text: `${tf("Total")} ${rows.totalText}`, }), el("p", { className: "m01-logic__muted", text: `${tf("TotalNote")}: ${roundText(totalRound)}`, }), ...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })), ]); /** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */ const linesOf = (id: string): FormulaLine[] | null => { const box = lg.boxes.find((b) => b.id === id); if (!box?.func) return null; try { return formulaLines(box, cellValuesOf(box, ev.arrived?.[id])); } catch { return null; } }; const bodies = rows.funcs.map((f) => funcBlock(f, linesOf(f.box))); const text = section( tf("Formula"), bodies.length ? bodies : [el("p", { className: "m01-logic__muted", text: tf("NoFunc") })], ); host.replaceChildren(detail, text); }