- FlowRows(순수): 출력 상자를 비용 종류별 줄 · 소계 · 계(합계 끝수 1원 미만 버림) · 함수 상자 결과 - FlowView: 화면 · sub2 식 보기가 있으면 식 두 줄 - Detail: DetailContext.flowHost 가 있으면 호표 · 읽는 식 대신 이 칸 - 시험 test_m01_logic_flowview.py Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
208 lines
7.6 KiB
TypeScript
208 lines
7.6 KiB
TypeScript
/* =============================================================================
|
|
* 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 type { CfBox, CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
|
|
import { itemRoundNow, totalRoundNow } 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: ["합계 끝수 기본 = 1원 미만 버림", "Total rounding default: floor to 1 won"],
|
|
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<FlowGroupKey, TextKey> = {
|
|
labor: "Labor",
|
|
material: "Material",
|
|
cost: "Cost",
|
|
plain: "Plain",
|
|
};
|
|
|
|
/** sub2 의 식 보기(`M02_DF_FormulaView.ts` · 정함 8) — 아직 없으면 비어 있음 */
|
|
interface FormulaLine {
|
|
name: string;
|
|
named: string;
|
|
valued: string;
|
|
branches?: { when: string; then: string; now: boolean }[];
|
|
}
|
|
type FormulaFn = (box: CfBox, values: CfValues) => FormulaLine[];
|
|
const FORMULA = import.meta.glob<{ formulaLines?: FormulaFn }>(
|
|
"../M02_MasterTemplete/DataFlow/M02_DF_FormulaView.ts",
|
|
);
|
|
const loadFormula = async (): Promise<FormulaFn | null> => {
|
|
const load = Object.values(FORMULA)[0];
|
|
return load ? ((await load()).formulaLines ?? null) : null;
|
|
};
|
|
|
|
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 rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRoundNow() });
|
|
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") }),
|
|
...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })),
|
|
]);
|
|
const bodies = rows.funcs.map((f) => funcBlock(f, null));
|
|
const text = section(
|
|
tf("Formula"),
|
|
bodies.length ? bodies : [el("p", { className: "m01-logic__muted", text: tf("NoFunc") })],
|
|
);
|
|
host.replaceChildren(detail, text);
|
|
// sub2 식 보기가 올라와 있으면 함수 상자마다 식 두 줄을 더함
|
|
void loadFormula().then((fn) => {
|
|
if (!fn || !bodies.length) return;
|
|
const boxes = lg.boxes.filter((b) => b.kind === "function" && b.func);
|
|
const merged = { ...ev.ports, ...ev.arrived } as CfValues;
|
|
text.replaceChildren(
|
|
text.firstElementChild!,
|
|
...rows.funcs.map((f, i) => {
|
|
try {
|
|
return funcBlock(f, fn(boxes[i], merged));
|
|
} catch {
|
|
return bodies[i];
|
|
}
|
|
}),
|
|
);
|
|
});
|
|
}
|