Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
215 lines
7.1 KiB
TypeScript
215 lines
7.1 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Test_A.ts
|
|
* 방식 A — 질문·답 마법사: 공종 → 조건 질문(한 번에 하나) → 재료·기계 고르기 → 결과 호표
|
|
* 식은 안 보임 · 시험 계산은 로직 화면과 같은 `/calc` · 저장 없음
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
|
import {
|
|
fetchLogic,
|
|
runCalc,
|
|
type CalcAnswer,
|
|
type ElementBrief,
|
|
type LogicInput,
|
|
type LogicOne,
|
|
} from "./M01_MasterData_UI_Logic_Api";
|
|
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
|
import { hasPickable, pickPanel, swappedRow } from "./M01_MasterData_UI_Test_Pick";
|
|
import { guideLines, plainReason, tt } from "./M01_MasterData_UI_Test_Text";
|
|
|
|
type Step =
|
|
{ kind: "job" } | { kind: "ask"; spec: LogicInput } | { kind: "pick" } | { kind: "result" };
|
|
|
|
export function render(host: HTMLElement, logicKey: string): void {
|
|
host.replaceChildren(el("p", { className: "m01-test__muted", text: tt("Loading") }));
|
|
fetchLogic(logicKey)
|
|
.then((one) => wizard(host, one))
|
|
.catch((error) => showToast(error instanceof Error ? error.message : tt("Failed"), "error"));
|
|
}
|
|
|
|
function wizard(host: HTMLElement, one: LogicOne): void {
|
|
const { logic } = one;
|
|
const values: Record<string, string> = {};
|
|
const swaps = new Map<number, ElementBrief>();
|
|
const steps: Step[] = [
|
|
{ kind: "job" },
|
|
...(logic.입력 ?? []).map((spec): Step => ({ kind: "ask", spec })),
|
|
...(hasPickable(logic) ? [{ kind: "pick" } as Step] : []),
|
|
{ kind: "result" },
|
|
];
|
|
const picker = pickPanel(one, swaps, () => undefined);
|
|
let at = 0;
|
|
|
|
const go = (next: number): void => {
|
|
at = next;
|
|
draw();
|
|
};
|
|
|
|
const frame = (
|
|
title: string,
|
|
note: string,
|
|
content: HTMLElement[],
|
|
nextOk: boolean,
|
|
): HTMLButtonElement => {
|
|
const last = steps[at].kind === "result";
|
|
const back = createButton({ label: tt("Back"), variant: "ghost", onClick: () => go(at - 1) });
|
|
back.disabled = at === 0;
|
|
const next = createButton({ label: tt("Next"), onClick: () => go(at + 1) });
|
|
next.disabled = !nextOk;
|
|
const again = createButton({ label: tt("Again"), variant: "ghost", onClick: () => go(0) });
|
|
host.replaceChildren(
|
|
el("div", {
|
|
className: "m01-test__card",
|
|
children: [
|
|
el("p", {
|
|
className: "m01-test__step",
|
|
text: tt("Step", { n: at + 1, total: steps.length }),
|
|
}),
|
|
el("h3", { text: title }),
|
|
...(note ? [el("p", { className: "m01-test__muted", text: note })] : []),
|
|
...content,
|
|
el("div", { className: "m01-test__nav", children: last ? [back, again] : [back, next] }),
|
|
],
|
|
}),
|
|
);
|
|
return next;
|
|
};
|
|
|
|
const draw = (): void => {
|
|
const step = steps[at];
|
|
if (step.kind === "job") {
|
|
frame(
|
|
`${logic.원문번호} ${logic.이름}`,
|
|
tt("Job_Note"),
|
|
[
|
|
el("p", { text: `${tt("Job_Title")} · ${logic.결과단위}` }),
|
|
el("ul", { children: guideLines("A").map((t) => el("li", { text: t })) }),
|
|
],
|
|
true,
|
|
);
|
|
} else if (step.kind === "ask") drawAsk(step.spec);
|
|
else if (step.kind === "pick") drawPick();
|
|
else void drawResult();
|
|
};
|
|
|
|
const drawAsk = (spec: LogicInput): void => {
|
|
const name = spec.이름;
|
|
const ready = (): boolean => (values[name] ?? "").trim() !== "";
|
|
let next: HTMLButtonElement | null = null;
|
|
const changed = (v: string): void => {
|
|
values[name] = v;
|
|
if (next) next.disabled = !ready();
|
|
};
|
|
let control: HTMLElement;
|
|
if (spec.고르기?.length) {
|
|
control = createSelectField({
|
|
options: ["", ...spec.고르기.map(String)].map((o) => ({ value: o, text: o })),
|
|
value: values[name] ?? "",
|
|
onChange: changed,
|
|
}).root;
|
|
} else {
|
|
const box = el("input", {
|
|
className: "m01-test__input",
|
|
attrs: { type: "text", inputmode: "decimal", placeholder: tt("Enter_Value") },
|
|
});
|
|
box.value = values[name] ?? "";
|
|
box.addEventListener("input", () => changed(box.value));
|
|
control = box;
|
|
}
|
|
const question = spec.고르기?.length
|
|
? tt("Ask_Pick", { name })
|
|
: tt("Ask_Number", { name, unit: spec.단위 ? ` (${spec.단위})` : "" });
|
|
const hint = spec.범위 ? tt("Ask_Range", { min: spec.범위[0], max: spec.범위[1] }) : "";
|
|
next = frame(question, hint, [control], ready());
|
|
};
|
|
|
|
const drawPick = (): void => {
|
|
frame(tt("Mat_Title"), tt("Mat_Note"), [picker], true);
|
|
};
|
|
|
|
const drawResult = async (): Promise<void> => {
|
|
const inputs: Record<string, unknown> = {};
|
|
for (const spec of logic.입력 ?? []) {
|
|
const raw = (values[spec.이름] ?? "").trim();
|
|
if (raw === "") continue;
|
|
const option = spec.고르기?.find((o) => String(o) === raw);
|
|
inputs[spec.이름] =
|
|
option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw);
|
|
}
|
|
const row = swappedRow(logic, swaps); // 바꿔 본 것이 있으면 복사본으로만 셈
|
|
let answer: CalcAnswer;
|
|
try {
|
|
answer = await runCalc({ key: logic.키, inputs, ...(row ? { row, file: one.file } : {}) });
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : tt("Failed"), "error");
|
|
return;
|
|
}
|
|
frame(tt("Result_Title"), "", resultView(answer), true);
|
|
};
|
|
|
|
draw();
|
|
}
|
|
|
|
function resultView(answer: CalcAnswer): HTMLElement[] {
|
|
if (!answer.ok) {
|
|
return [
|
|
el("div", {
|
|
className: "m01-test__reasons",
|
|
children: [
|
|
el("strong", { text: tt("Result_Stopped") }),
|
|
el("div", { text: plainReason(answer.reason) }),
|
|
],
|
|
}),
|
|
];
|
|
}
|
|
const out: HTMLElement[] = [];
|
|
if (answer.lines) {
|
|
out.push(
|
|
el("table", {
|
|
className: "m01-test__grid",
|
|
children: [
|
|
el("thead", {
|
|
children: [
|
|
el("tr", {
|
|
children: [tt("Col_Name"), tt("Col_Qty"), tt("Col_Amount")].map((h) =>
|
|
el("th", { text: h }),
|
|
),
|
|
}),
|
|
],
|
|
}),
|
|
el("tbody", {
|
|
children: answer.lines.map((l) =>
|
|
el("tr", {
|
|
children: [
|
|
el("td", { text: l.출처 ? `${l.이름} (${l.출처})` : l.이름 }),
|
|
el("td", { text: `${formatNumber(l.수량)} ${l.단위}` }),
|
|
el("td", { className: "m01-test__money", text: formatNumber(l.금액) }),
|
|
],
|
|
}),
|
|
),
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
}
|
|
if (answer.sums) {
|
|
out.push(
|
|
el("p", {
|
|
className: "m01-test__total",
|
|
attrs: { "data-total": String(answer.sums["계"] ?? 0) },
|
|
text: `${tt("Sum_Total")} ${formatNumber(answer.sums["계"] ?? 0)}`,
|
|
}),
|
|
);
|
|
}
|
|
if (answer.result !== undefined) {
|
|
out.push(
|
|
el("p", {
|
|
className: "m01-test__total",
|
|
attrs: { "data-total": String(answer.result) },
|
|
text: `${tt("Sum_Result")} ${formatNumber(answer.result)}`,
|
|
}),
|
|
);
|
|
}
|
|
return out;
|
|
}
|