/* ============================================================================= * M01_MasterData_UI_Test_A.ts * 방식 A — 질문·답 마법사: 공종 → 조건 질문(한 번에 하나) → 재료·기계 고르기 → 결과 호표 * 식은 안 보임 · 시험 계산은 로직 화면과 같은 `/calc` · 저장 없음 * ========================================================================== */ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { fetchLogic, runCalc, searchElements, type CalcAnswer, type ElementBrief, type HoLine, type LogicInput, type LogicOne, } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { tt } from "./M01_MasterData_UI_Test_Text"; type Step = { kind: "job" } | { kind: "ask"; spec: LogicInput } | { kind: "pick" } | { kind: "result" }; const PICKABLE = ["재료", "기계"]; 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 lines = (logic.호표 ?? []) .map((line, i) => ({ line, i })) .filter((x) => PICKABLE.includes(x.line.종류)); const steps: Step[] = [ { kind: "job" }, ...(logic.입력 ?? []).map((spec): Step => ({ kind: "ask", spec })), ...(lines.length ? [{ kind: "pick" } as Step] : []), { kind: "result" }, ]; 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.결과단위}` })], 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" }, }); 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 => { const rows = lines.map(({ line, i }) => pickRow(line, i)); frame( tt("Mat_Title"), tt("Mat_Note"), rows.length ? rows : [el("p", { text: tt("Mat_None") })], true, ); }; const pickRow = (line: HoLine, i: number): HTMLElement => { const price = one.prices[line.요소]?.값; const found = new Map(); const select = createSelectField({ options: [{ value: "", text: tt("Mat_Default") }], value: "", onChange: (ref) => { const item = found.get(ref); if (item) swaps.set(i, item); else swaps.delete(i); }, }); void searchElements(line.종류, line.이름 ?? "") .then((r) => { r.items.forEach((it) => found.set(it.ref, it)); select.setOptions( [ { value: "", text: tt("Mat_Default") }, ...r.items.map((it) => ({ value: it.ref, text: [`${it.이름 ?? ""} ${it.규격 ?? ""}`.trim(), formatNumber(it.값)] .filter(Boolean) .join(" · "), })), ], swaps.get(i)?.ref ?? "", ); }) .catch(() => undefined); const spec = line.규격 ? ` ${line.규격}` : ""; const unit = line.단위 ?? ""; const cost = price !== undefined && price !== null ? ` · ${tt("Mat_Price")} ${formatNumber(price)}` : ""; return el("div", { className: "m01-test__pick", children: [ el("strong", { text: `${line.종류} · ${line.이름 ?? line.요소}${spec}` }), el("span", { className: "m01-test__muted", text: `${unit}${cost}` }), select.root, ], }); }; 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 = swaps.size ? { ...logic, 호표: (logic.호표 ?? []).map((l, i) => { const s = swaps.get(i); return s ? { ...l, 요소: s.ref, 이름: s.이름 ?? l.이름, 규격: s.규격 ?? l.규격 } : l; }), } : undefined; 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: 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; }