- 화면 [계산] — 고르기 값은 원래 값(수면 수) 그대로 보냄 · 고르기 칸 고칠 때 모두 수면 수로 저장 - 엔진 입력 — 글로 온 수를 고르기 원래 값·Decimal 로 받음 - 저장 — 안 바뀐 요소는 옛 글 그대로(한 줄 요소 한 줄 · 들여쓰기 · 줄바꿈) · 고친 요소만 새로 씀 - 시험 — 13-4-1 글 입력 계산 · 저장 차이가 고친 줄만 · 3-3-1 시험에 지역·보정작업 입력 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
/* =============================================================================
|
||
* M01_MasterData_UI_Logic_Calc.ts
|
||
* 시험 계산 — 받을 값을 넣고 [계산] → 줄별 금액 · 비목 합 · 멈춘 까닭(엔진 글 그대로)
|
||
* 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀
|
||
* ========================================================================== */
|
||
|
||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||
import {
|
||
runCalc,
|
||
type CalcAnswer,
|
||
type CalcLine,
|
||
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 {
|
||
book: string;
|
||
/** 저장된 열쇠 — 새 로직은 null */
|
||
savedKey: string | null;
|
||
file: string;
|
||
row: LogicRow;
|
||
dirty: () => boolean;
|
||
/** 로직마다 넣은 값 — 다시 그려도 남음 */
|
||
values: Record<string, string>;
|
||
onLines: (lines: CalcLine[] | null) => void;
|
||
}
|
||
|
||
const SUMS = ["노무비", "재료비", "경비", "계"];
|
||
|
||
export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||
const fields = (ctx.row.입력 ?? []).map((spec) => {
|
||
const name = spec.이름;
|
||
let control: HTMLInputElement | HTMLSelectElement;
|
||
if (spec.고르기?.length) {
|
||
control = el("select", { className: "m01-logic__input" });
|
||
for (const option of ["", ...spec.고르기.map(String)]) {
|
||
control.append(el("option", { text: option, attrs: { value: option } }));
|
||
}
|
||
} else {
|
||
control = el("input", {
|
||
className: "m01-logic__input",
|
||
attrs: { type: "text", inputmode: "decimal" },
|
||
});
|
||
if (spec.범위) control.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||
}
|
||
control.value = ctx.values[name] ?? "";
|
||
control.addEventListener("input", () => (ctx.values[name] = control.value));
|
||
control.addEventListener("change", () => (ctx.values[name] = control.value));
|
||
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 answer = await runCalc({
|
||
book: ctx.book,
|
||
key: ctx.savedKey ?? ctx.row.열쇠,
|
||
inputs,
|
||
...(draft ? { row: ctx.row, file: ctx.file } : {}),
|
||
});
|
||
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.이름 }),
|
||
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;
|
||
}
|