Files
Aislo/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts
T
eomsangdonandClaude Sonnet 5 21f04c64bb feat(M01): 화면 부품 공용화 — 드롭다운·찾기 칸·단추·확인창을 공용 템플릿으로 · 재료 컨테이너 차례
- createSelectField 에 선택 인자 더함(compact · ariaLabel · setOptions) — 기본 동작 그대로
- 인력 2단 거름 · 로직 편집 고르기 · 계산 입력 · 요소 찾기 창 그룹 select 를 공용으로 · m01-side__select 걷음
- 찾기 칸은 createInputField · 표 칸·모달 단추는 createButton · 버리기 확인은 showConfirmDialog
- 재료 컨테이너 = 자재품목 · 유가 · 품셈재료 차례 · 안 쓰는 조달 문구 걷음

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
2026-09-20 16:27:06 +09:00

171 lines
5.7 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,
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: 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 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.출처 ? `${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;
}