Files
Aislo/M01_MasterData/M01_MasterData_UI_Test_A.ts
T
eomsangdonandClaude Opus 5 98e40f4496 feat(M01): 일위대가 로직 기본 보기를 흐름 그림으로 · 상자에서 보고 고치기
방식 C 를 테스트 컨테이너에서 꺼내 로직 화면 기본 보기로 올림(옛 호표 표는 「고급」).
상자를 누르면 값·출처에 더해 쉬운 말 풀이와 원문 표 미리보기(걸린 줄 강조)가 펼쳐짐.
자체 로직은 [고치기] 로 상자 안에서 고치고 줄을 더하거나 지움 — 정본은 「본떠 만들기」 뒤.

- UI_Test_C·_Model·_Style → UI_Logic_Flow·_Model·_Style
- UI_Test_Pick 의 재료·기계 바꿔 고르기를 UI_Logic_Pick 에 합침
- 방식 B 의 설명 카드 로직을 UI_Logic_Note 로 옮겨 흐름 그림과 같이 씀

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
2026-09-21 09:35:33 +09:00

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_Logic_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;
}