feat(M01): 관리자 화면 로직 칸 — 일위대가표(호표) 고치기 · 시험 계산 · 요소 찾기 창
- 화면: 왼쪽 로직 목록(원문·장·이름 찾기·막힘) / 가운데 머리·받을 값·호표·중간 값·덧줄·끝수 / 오른쪽 시험 계산 - 고친 것은 캐시에 쌓고 [저장] 한 번에 · [고친 것 버리기] · 줄 더하기·지우기 · 로직 새로 만들기·지우기 - 진입은 mountM01Logic(host) — 공용 진입 파일(sub_laptop_3 몫)이 한 줄로 붙임 - 서버: 로직 하나에 단가 자동(prices) · 요소 찾기(GET /elements) · 저장 전 시험 계산(calc 에 row·file) - 계약 문서 갱신 · 시험 1개 더함(9개 통과) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/* =============================================================================
|
||||
* 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.고르기]) {
|
||||
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; // 빈 칸은 안 보냄 — 엔진이 「입력 없음」 으로 멈춤
|
||||
inputs[spec.이름] = spec.고르기?.length || 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;
|
||||
}
|
||||
Reference in New Issue
Block a user