Files
Aislo/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts
T

176 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M01_MasterData_UI_Logic_Calc.ts
* 시험 계산 — 받을 값을 넣고 [계산] → 줄별 금액 · 비목 합 · 멈춘 까닭(엔진 글 그대로)
* 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀
* ========================================================================== */
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
import {
runCalc,
runText,
type CalcAnswer,
type CalcLine,
type TextAnswer,
type LogicRow,
} from "./M01_MasterData_UI_Logic_Api";
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import { tx } from "./M01_MasterData_UI_Logic_Text";
export interface CalcContext {
/** 저장된 키 — 새 로직은 null */
savedKey: string | null;
/** 저장 전 새 초안이면 비움 — 서버가 임시 자리에 얹어 셈 */
file?: string;
row: LogicRow;
dirty: () => boolean;
/** 로직마다 넣은 값 — 다시 그려도 남음 */
values: Record<string, string>;
onLines: (lines: CalcLine[] | null) => void;
/** 읽는 식 줄(`/text`)도 같이 받고 싶을 때 — 호출이 `onLines` 보다 먼저 */
onText?: (answer: TextAnswer) => void;
}
const SUMS = ["노무비", "재료비", "경비", "계"];
export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
const fields = (ctx.row.입력 ?? []).map((spec) => {
const name = spec.이름;
let control: HTMLElement;
if (spec.고르기?.length) {
const pick = createSelectField({
options: ["", ...spec.고르기.map(String)].map((o) => ({ value: o, text: o })),
value: ctx.values[name] ?? "",
compact: true,
onChange: (v) => (ctx.values[name] = v),
});
control = pick.root;
} else {
const box = el("input", {
className: "m01-logic__input",
attrs: { type: "text", inputmode: "decimal" },
});
if (spec.범위) box.placeholder = `${spec.범위[0]} ${spec.범위[1]}`;
box.value = ctx.values[name] ?? "";
box.addEventListener("input", () => (ctx.values[name] = box.value));
control = box;
}
const label = spec.단위 ? `${name} (${spec.단위})` : name;
return el("label", {
className: "m01-logic__field",
children: [el("span", { text: label }), control],
});
});
const out = el("div", { className: "m01-logic__calc-out" });
const run = async (): Promise<void> => {
const inputs: Record<string, unknown> = {};
for (const spec of ctx.row.입력 ?? []) {
const raw = (ctx.values[spec.이름] ?? "").trim();
if (raw === "") continue; // 빈 칸은 안 보냄 — 엔진이 「입력 없음」 으로 멈춤
// 고르기는 원래 값(수면 수) 그대로 — 글 "35" 로 보내면 「고르기 밖」
const option = spec.고르기?.find((o) => String(o) === raw);
inputs[spec.이름] =
option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw);
}
const draft = ctx.dirty() || ctx.savedKey === null;
try {
const body = {
key: ctx.savedKey ?? ctx.row.,
inputs,
...(draft ? { row: ctx.row, file: ctx.file } : {}),
};
const answer = await runCalc(body);
if (ctx.onText) ctx.onText(await runText(body));
ctx.onLines(answer.ok ? (answer.lines ?? null) : null);
out.replaceChildren(...answerView(answer, draft));
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
}
};
host.replaceChildren(
el("h3", { text: tx("Calc_Title") }),
...(fields.length
? fields
: [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]),
createButton({ label: tx("Calc_Run"), onClick: () => void run() }),
out,
);
}
function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : [];
if (!answer.ok) {
return [
...note,
el("div", {
className: "m01-logic__reasons",
children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: answer.reason })],
}),
];
}
const parts: HTMLElement[] = [...note];
if (answer.lines) {
const rows = answer.lines.map((line) =>
el("tr", {
children: [
el("td", { text: line.출처 ? `${line.이름} (${line.출처})` : line.이름 }),
el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }),
el("td", { className: "m01-logic__money", text: formatNumber(line.금액) }),
],
}),
);
parts.push(
el("table", {
className: "m01-logic__grid",
children: [
el("thead", {
children: [
el("tr", {
children: [tx("Head_Name"), tx("Ho_Qty"), tx("Ho_Amount")].map((h) =>
el("th", { text: h }),
),
}),
],
}),
el("tbody", { children: rows }),
],
}),
);
}
if (answer.sums) {
parts.push(
el("dl", {
className: "m01-logic__sums",
children: SUMS.flatMap((k) => [
el("dt", { text: k === "계" ? tx("Calc_Sum") : k }),
el("dd", { className: "m01-logic__money", text: formatNumber(answer.sums?.[k] ?? 0) }),
]),
}),
);
}
if (answer.result !== undefined) {
parts.push(
el("dl", {
className: "m01-logic__sums",
children: [
el("dt", { text: tx("Calc_Result") }),
el("dd", { text: formatNumber(answer.result) }),
],
}),
);
}
const middle = Object.entries(answer.middle ?? {});
if (middle.length) {
parts.push(
el("h4", { text: tx("Middle_Title") }),
el("dl", {
className: "m01-logic__sums",
children: middle.flatMap(([k, v]) => [
el("dt", { text: k }),
el("dd", { text: formatNumber(v) }),
]),
}),
);
}
return parts;
}