Files
Aislo/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts
T
eomsangdonandClaude Opus 5 6c9ac97b29 feat(master_data): 키 개편 — 테이블ID+6자리 키 · 인력·기계 한 테이블 · 장 파일 이름
- 모든 요소·표·로직 줄의 열쇠 → 키(LB000123 꼴) · 옛 열쇠·원문 번호는 원문번호 칸 · 대장 _키대장.json (키 36,840 · 다음 번호)
- 변수 적는 법 키로 — 로직 식·연결·준용·통합·후보·조달 연결 일괄 변환 · {이름} 낀 참조는 ID:원문번호
- 인력 8 파일 → 인력.json(줄마다 조사 · 머리 조사 묶음) · 기계 2 파일 → 기계.json(세부분류)
- 소요량·계수·로직 파일 이름 = 그룹_원문_NN장_장 제목 · 머리 부문·차례
- 엔진(값 찾기 · 알림에 키 옆 이름) · check_master(키 모양·겹침·대장·끊긴 키 · 기계 요소 줄도 본문 결손 대조) · M01 서버(새 줄 키는 대장 다음 번호 · 키 못 고침) · 화면 칸 이름만 맞춤
- 로직 일괄 시험 계산 1,351 줄 — 옮기기 전후 줄마다 결과·금액 같음

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-20 01:56:30 +09:00

168 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, 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.출처 ? `${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;
}