/* ============================================================================= * M01_MasterData_UI_LogicLab_FlowRows.ts * 흐름 로직의 왼쪽 「단가산출 상세」 줄 만들기(DOM 없음 · 순수) — 흐름도 출력 상자를 비용 종류별로 묶어 * 줄(이름 · 단위 · 값) · 소계 · 계 · 함수 상자별 결과 값. 값은 DataFlow 계산 결과(`CfEvalResult`)를 읽기만 함. * 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7. 시험 = resources/tester/test_m01_logic_flowview.py * ========================================================================== */ import type { CfEvalResult } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval"; import type { CfBox, CfCost, CfLogic, CfRound } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; import { roundBy, showRounded } from "../M02_MasterTemplete/DataFlow/M02_DF_Round"; export type FlowGroupKey = "labor" | "material" | "cost" | "plain"; /** 비용 종류 → 묶음(인력 = 노무비 · 재료 = 재료비 · 경비) · 값(비용 아님)은 따로 */ export const GROUP_OF: Record = { 노무비: "labor", 재료비: "material", 경비: "cost", 값: "plain", }; export const GROUP_ORDER: FlowGroupKey[] = ["labor", "material", "cost", "plain"]; const COST_OF: Record, "노무비" | "재료비" | "경비"> = { labor: "노무비", material: "재료비", cost: "경비", }; export interface FlowLine { box: string; name: string; unit: string; value: number | string | boolean | null; /** 보이는 글 — 숫자는 그 출력의 끝수 자리대로 */ text: string; } export interface FlowGroup { key: FlowGroupKey; lines: FlowLine[]; /** 비용 묶음만 — 합계 끝수를 적용한 소계 */ sub: number | null; subText: string; } export interface FlowFunc { box: string; name: string; unit: string; value: unknown; text: string; } export interface FlowRows { groups: FlowGroup[]; /** 합계 끝수(기본 1원 미만 버림)를 적용한 계 */ total: number; totalText: string; funcs: FlowFunc[]; errors: string[]; } const plain = (v: unknown): string => v === null || v === undefined || Array.isArray(v) ? "—" : typeof v === "number" ? v.toLocaleString("ko-KR", { maximumFractionDigits: 6 }) : String(v); /** 줄 모으기 — round.item = 출력 기본 끝수 · round.total = 합계 기본 끝수 */ export function flowRows( lg: CfLogic, ev: CfEvalResult, round: { item: CfRound; total: CfRound | null }, ): FlowRows { const outs: CfBox[] = lg.boxes.filter((b) => b.kind === "output"); const groups = GROUP_ORDER.map((key): FlowGroup => { const mine = outs.filter((b) => GROUP_OF[b.output?.cost ?? "값"] === key); const lines = mine.map((b): FlowLine => { const v = ev.outputs[b.id] ?? null; const r = b.output?.round ?? (key === "plain" ? undefined : round.item); return { box: b.id, name: b.name, unit: b.output?.unit ?? b.unit, value: v, text: typeof v === "number" ? showRounded(v, r) : plain(v), }; }); const raw = key === "plain" ? null : ev.cost[COST_OF[key]]; const sub = raw === null ? null : roundBy(round.total, raw); return { key, lines, sub, subText: sub === null ? "" : showRounded(sub, round.total ?? undefined), }; }).filter((g) => g.lines.length); const total = roundBy(round.total, ev.cost.계); const funcs = lg.boxes .filter((b) => b.kind === "function" && b.func) .map((b): FlowFunc => { const out = b.func!.output; const v = ev.ports[b.id]?.[out.name]; return { box: b.id, name: b.name, unit: out.unit || b.unit, value: v, text: typeof v === "number" ? v.toLocaleString("ko-KR", { maximumFractionDigits: 6 }) : plain(v), }; }); return { groups, total, totalText: showRounded(total, round.total ?? undefined), funcs, errors: ev.errors, }; }