/* ============================================================================= * 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 = {}; const swaps = new Map(); 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 => { const inputs: Record = {}; 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; }