feat(M01): 시험 계산 재료 · 기계 고르기를 마스터 표 모달로 — 품목 고르기 · 고름 실어 다시 계산 · 단위경고 딱지 (PLAN 8-4)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -12,9 +12,16 @@ import {
|
||||
searchElements,
|
||||
type CalcAnswer,
|
||||
type CalcLine,
|
||||
type ElementBrief,
|
||||
type TextAnswer,
|
||||
type LogicRow,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import {
|
||||
isMaterialKeyInput,
|
||||
machineControl,
|
||||
materialRows,
|
||||
pickBody,
|
||||
} from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
@@ -47,6 +54,8 @@ export interface CalcContext {
|
||||
/** 저장 전 새 초안이면 비움 — 서버가 임시 자리에 얹어 셈 */
|
||||
file?: string;
|
||||
row: LogicRow;
|
||||
/** 잡힌 품목을 보이는 데 씀 — 재료 줄의 단가 요소 · 조건 글 → 요소 */
|
||||
prices?: Record<string, ElementBrief | null>;
|
||||
dirty: () => boolean;
|
||||
/** 로직마다 넣은 값 — 다시 그려도 남음 */
|
||||
values: Record<string, string>;
|
||||
@@ -68,49 +77,56 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
// 지금 쓰는 LogicLab 화면은 그 칸에 안 걸려 폭이 이미 넓음(실측 730px) · 따로 안 풂
|
||||
const optionText = (o: string | number): string =>
|
||||
(typeof o === "string" && nameCache.get(o)) || String(o);
|
||||
const fields = (ctx.row.입력 ?? []).map((spec) => {
|
||||
const name = spec.이름;
|
||||
let control: HTMLElement;
|
||||
const picks = spec.고르기;
|
||||
if (picks?.length) {
|
||||
const pick = createSelectField({
|
||||
options: ["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
value: ctx.values[name] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => {
|
||||
ctx.values[name] = v;
|
||||
},
|
||||
});
|
||||
control = pick.root;
|
||||
const keys = picks.filter((o): o is string => typeof o === "string" && KEY_RE.test(o));
|
||||
if (keys.length) {
|
||||
void Promise.all(keys.map(keyName)).then(() => {
|
||||
pick.setOptions(
|
||||
["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
ctx.values[name] ?? "",
|
||||
);
|
||||
const again = (): void => host.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
||||
const fields = (ctx.row.입력 ?? [])
|
||||
.filter((spec) => !isMaterialKeyInput(spec)) // 재료 키 목록은 표 모달로 바뀌어 걷음
|
||||
.map((spec) => {
|
||||
const name = spec.이름;
|
||||
let control: HTMLElement;
|
||||
const picks = spec.고르기;
|
||||
const machine = machineControl(spec, ctx.values, again);
|
||||
if (machine) {
|
||||
control = machine;
|
||||
} else if (picks?.length) {
|
||||
const pick = createSelectField({
|
||||
options: ["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
value: ctx.values[name] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => {
|
||||
ctx.values[name] = v;
|
||||
},
|
||||
});
|
||||
control = pick.root;
|
||||
const keys = picks.filter((o): o is string => typeof o === "string" && KEY_RE.test(o));
|
||||
if (keys.length) {
|
||||
void Promise.all(keys.map(keyName)).then(() => {
|
||||
pick.setOptions(
|
||||
["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
ctx.values[name] ?? "",
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const box = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "text", inputmode: "decimal" },
|
||||
});
|
||||
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
box.value = ctx.values[name] ?? "";
|
||||
box.addEventListener("input", () => {
|
||||
ctx.values[name] = box.value;
|
||||
});
|
||||
control = box;
|
||||
}
|
||||
} else {
|
||||
const box = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "text", inputmode: "decimal" },
|
||||
const label = spec.단위 ? `${name} (${spec.단위})` : name;
|
||||
// 긴 이름 줄만 한 칸 다 씀 — 고르기 줄은 짧으면 그대로 2열에 맞춰 나란히
|
||||
const wide = label.length > 10;
|
||||
return el("label", {
|
||||
className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`,
|
||||
children: [el("span", { text: label }), control],
|
||||
});
|
||||
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
box.value = ctx.values[name] ?? "";
|
||||
box.addEventListener("input", () => {
|
||||
ctx.values[name] = box.value;
|
||||
});
|
||||
control = box;
|
||||
}
|
||||
const label = spec.단위 ? `${name} (${spec.단위})` : name;
|
||||
// 긴 이름 줄만 한 칸 다 씀 — 고르기 줄은 짧으면 그대로 2열에 맞춰 나란히
|
||||
const wide = label.length > 10;
|
||||
return el("label", {
|
||||
className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`,
|
||||
children: [el("span", { text: label }), control],
|
||||
});
|
||||
});
|
||||
const materials = materialRows(ctx.row, ctx.prices ?? {}, ctx.values, again);
|
||||
const out = el("div", { className: "m01-logic__calc-out" });
|
||||
const mySeqAtOpen = ctx.onText ? previewSeq + 1 : previewSeq; // 이 그리기가 받을 순번
|
||||
let manualRun = false; // [계산] 이 이미 답했으면 뒤늦게 오는 미리보기로 안 덮음
|
||||
@@ -131,7 +147,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
if (!fields.length && !autoAsked.has(ctx.values)) {
|
||||
autoAsked.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 계산
|
||||
queueMicrotask(() => {
|
||||
if (previewSeq === mySeqAtOpen) host.querySelector("button")?.click();
|
||||
if (previewSeq === mySeqAtOpen) again();
|
||||
});
|
||||
} else if (ctx.savedKey !== null && !autoAsked.has(ctx.values)) {
|
||||
autoAsked.add(ctx.values);
|
||||
@@ -142,7 +158,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
if (!names.length || previewSeq !== mySeqAtOpen) return; // 값이 없거나 다른 로직이 이미 열림
|
||||
for (const n of names) ctx.values[n] = String(auto[n]);
|
||||
buildCalc(host, ctx); // 채운 값을 새로 그리고 바로 계산(결과 · 텍스트 수식까지)
|
||||
host.querySelector("button")?.click();
|
||||
again();
|
||||
});
|
||||
}
|
||||
const run = async (): Promise<void> => {
|
||||
@@ -161,6 +177,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const body = {
|
||||
key: ctx.savedKey ?? ctx.row.키,
|
||||
inputs,
|
||||
...pickBody(ctx.values),
|
||||
...(draft ? { row: ctx.row, file: ctx.file } : {}),
|
||||
};
|
||||
const answer = await runCalc(body);
|
||||
@@ -171,12 +188,14 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
const runButton = createButton({ label: tx("Calc_Run"), onClick: () => void run() });
|
||||
runButton.setAttribute("data-calc-run", "");
|
||||
host.replaceChildren(
|
||||
el("h3", { text: tx("Calc_Title") }),
|
||||
...(fields.length
|
||||
? [el("div", { className: "m01-logic__head", children: fields })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열
|
||||
...(fields.length || materials.length
|
||||
? [el("div", { className: "m01-logic__head", children: [...fields, ...materials] })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열
|
||||
: [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]),
|
||||
createButton({ label: tx("Calc_Run"), onClick: () => void run() }),
|
||||
runButton,
|
||||
out,
|
||||
);
|
||||
}
|
||||
@@ -201,7 +220,19 @@ function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLEle
|
||||
const rows = answer.lines.map((line) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: line.출처 ? `${line.이름} (${line.출처})` : line.이름 }),
|
||||
el("td", {
|
||||
children: [
|
||||
el("span", { text: line.출처 ? `${line.이름} (${line.출처})` : line.이름 }),
|
||||
...(line.단위경고
|
||||
? [
|
||||
el("span", {
|
||||
className: "m01-logic__tag m01lab__warn",
|
||||
text: `${tx("Calc_UnitWarn")} · ${line.단위경고}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }),
|
||||
el("td", { className: "m01-logic__money", text: formatMoney(line.금액) }),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user