fix(M01): 일위대가 조합 화면 — 계약 9장대로 맞춤 · 수량은 저장하지 않고 미리 보기 때만 보냄
- 담은 로직 줄 = {로직, 메모} 뿐 · 입력값(수량)은 화면 안 메모로만 두고 조합에 안 저장
- 서버 길 실제 이름으로 맞춤: /combo/new · /combo/edit(판본) · /combo/delete(판본) · 422 검사 글·409 낡은 판본 안내
- 미리 보기는 화면이 로직마다 부르지 않고 POST /combo/preview 한 번에 받아 비목별 합계를 찍음
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Combo_Preview.ts
|
||||
* 조합 미리 보기 — 담은 로직 저마다 입력값으로 `/calc` 를 불러 노무비·재료비·경비로 갈라 합침(보기만 · 저장 없음)
|
||||
* 조합 미리 보기 — 담은 로직 저마다 받은 입력값을 서버 미리 보기(`POST /combo/preview`)에 실어 비목별 합계를 받음
|
||||
* 입력값(수량)은 이 화면 안에만 두고 조합에 저장하지 않음(계약 9장)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { fetchLogic, runCalc, type LogicInput } from "./M01_MasterData_UI_Logic_Api";
|
||||
import type { ComboLogic } from "./M01_MasterData_UI_Combo_Api";
|
||||
import { fetchLogic, type LogicInput } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { previewCombo, type Combo } from "./M01_MasterData_UI_Combo_Api";
|
||||
import { tc, type ComboTextKey } from "./M01_MasterData_UI_Combo_Text";
|
||||
|
||||
const COSTS = ["노무비", "재료비", "경비"] as const;
|
||||
type Costs = Record<(typeof COSTS)[number], number>;
|
||||
const LABEL: Record<(typeof COSTS)[number], ComboTextKey> = {
|
||||
노무비: "Prev_Labor",
|
||||
재료비: "Prev_Material",
|
||||
@@ -18,25 +18,28 @@ const LABEL: Record<(typeof COSTS)[number], ComboTextKey> = {
|
||||
|
||||
const num = (v: string): number | string => (v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v);
|
||||
const fmt = (n: number): string => n.toLocaleString("ko-KR", { maximumFractionDigits: 4 });
|
||||
const total = (c: Costs): number => c.노무비 + c.재료비 + c.경비;
|
||||
const blank = (): Costs => ({ 노무비: 0, 재료비: 0, 경비: 0 });
|
||||
|
||||
/** 로직 입력 정의 → 처음 값(고르기 첫째 · 범위 아래끝) */
|
||||
const initial = (i: LogicInput): string => String(i.고르기?.[0] ?? i.범위?.[0] ?? "");
|
||||
|
||||
/** 입력 칸 — 바꾸면 그 줄의 입력에 바로 적음(조합과 함께 저장됨) */
|
||||
function inputCell(input: LogicInput, row: ComboLogic): HTMLElement {
|
||||
const value = row.입력[input.이름] ?? initial(input);
|
||||
row.입력[input.이름] = value;
|
||||
/** 입력 칸 — 바꾼 값은 `values`(이 화면 메모)에만 적음 */
|
||||
function inputCell(
|
||||
input: LogicInput,
|
||||
key: string,
|
||||
values: Record<string, Record<string, string>>,
|
||||
): HTMLElement {
|
||||
const mine = (values[key] ??= {});
|
||||
const value = mine[input.이름] ?? initial(input);
|
||||
mine[input.이름] = value;
|
||||
const choices = input.고르기?.map((v) => ({ value: String(v), text: String(v) }));
|
||||
const field = choices
|
||||
? createSelectField({ options: choices, value, compact: true, label: input.이름 })
|
||||
: createInputField({ type: "text", label: input.이름 });
|
||||
const control = "select" in field ? field.select : field.input;
|
||||
control.value = value;
|
||||
control.setAttribute("data-input", `${row.키}|${input.이름}`);
|
||||
control.addEventListener("input", () => (row.입력[input.이름] = control.value));
|
||||
control.addEventListener("change", () => (row.입력[input.이름] = control.value));
|
||||
control.setAttribute("data-input", `${key}|${input.이름}`);
|
||||
control.addEventListener("input", () => (mine[input.이름] = control.value));
|
||||
control.addEventListener("change", () => (mine[input.이름] = control.value));
|
||||
return field.root;
|
||||
}
|
||||
|
||||
@@ -46,8 +49,11 @@ export interface PreviewHandle {
|
||||
redraw: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function buildPreview(rows: () => ComboLogic[]): PreviewHandle {
|
||||
const inputs = el("div", { className: "m01-logic__stack" });
|
||||
export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
const values: Record<string, Record<string, string>> = {};
|
||||
/** 서버가 받을 꼴 — 원래 숫자·고르기 값으로 되돌림 */
|
||||
const inputs = new Map<string, LogicInput[]>();
|
||||
const form = el("div", { className: "m01-logic__stack" });
|
||||
const out = el("div", { attrs: { "data-combo": "preview" } });
|
||||
const rowOf = (cells: string[], note = "", bold = false): HTMLElement =>
|
||||
el("tr", {
|
||||
@@ -57,34 +63,43 @@ export function buildPreview(rows: () => ComboLogic[]): PreviewHandle {
|
||||
|
||||
const run = async (): Promise<void> => {
|
||||
out.replaceChildren(el("p", { className: "m01-logic__muted", text: "…" }));
|
||||
const sum = blank();
|
||||
const trs: HTMLElement[] = [];
|
||||
for (const row of rows()) {
|
||||
const cost = blank();
|
||||
let name = row.키;
|
||||
let note = "";
|
||||
try {
|
||||
const one = await fetchLogic(row.키);
|
||||
name = one.logic.이름;
|
||||
const values: Record<string, unknown> = {};
|
||||
for (const i of one.logic.입력) {
|
||||
const raw = row.입력[i.이름] ?? initial(i);
|
||||
if (raw.trim() === "") continue; // 빈 칸은 안 보냄(로직 화면과 같음)
|
||||
values[i.이름] = i.고르기?.find((o) => String(o) === raw) ?? num(raw);
|
||||
}
|
||||
const got = await runCalc({ key: row.키, inputs: values });
|
||||
if (got.ok) {
|
||||
for (const line of got.lines ?? []) for (const c of COSTS) cost[c] += line.비목?.[c] ?? 0;
|
||||
} else note = `${tc("Prev_Fail")}: ${got.reason}`;
|
||||
} catch (error) {
|
||||
note = `${tc("Prev_Fail")}: ${error instanceof Error ? error.message : ""}`;
|
||||
const send: Record<string, Record<string, unknown>> = {};
|
||||
for (const [key, defs] of inputs) {
|
||||
send[key] = {};
|
||||
for (const i of defs) {
|
||||
const raw = values[key]?.[i.이름] ?? initial(i);
|
||||
if (raw.trim() === "") continue; // 빈 칸은 안 보냄(로직 화면과 같음)
|
||||
send[key][i.이름] = i.고르기?.find((o) => String(o) === raw) ?? num(raw);
|
||||
}
|
||||
for (const c of COSTS) sum[c] += cost[c];
|
||||
trs.push(
|
||||
rowOf([`${row.키} ${name}`, ...COSTS.map((c) => fmt(cost[c])), fmt(total(cost))], note),
|
||||
);
|
||||
}
|
||||
trs.push(rowOf([tc("Prev_Sum"), ...COSTS.map((c) => fmt(sum[c])), fmt(total(sum))], "", true));
|
||||
let got;
|
||||
try {
|
||||
got = await previewCombo(combo(), send);
|
||||
} catch (error) {
|
||||
out.replaceChildren(
|
||||
el("p", { text: `${tc("Prev_Fail")}: ${error instanceof Error ? error.message : ""}` }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const trs = got.줄.map((l) =>
|
||||
rowOf(
|
||||
[
|
||||
`${l.로직} ${l.이름}`,
|
||||
fmt(l.노무비 ?? 0),
|
||||
fmt(l.재료비 ?? 0),
|
||||
fmt(l.경비 ?? 0),
|
||||
fmt(l.계 ?? 0),
|
||||
],
|
||||
l.ok ? "" : `${tc("Prev_Fail")}: ${l.까닭 ?? ""}`,
|
||||
),
|
||||
);
|
||||
trs.push(
|
||||
rowOf(
|
||||
[tc("Prev_Sum"), fmt(got.노무비), fmt(got.재료비), fmt(got.경비), fmt(got.계)],
|
||||
"",
|
||||
true,
|
||||
),
|
||||
);
|
||||
const heads = [tc("Prev_Row"), ...COSTS.map((c) => tc(LABEL[c])), tc("Prev_Sum")];
|
||||
out.replaceChildren(
|
||||
el("table", {
|
||||
@@ -108,26 +123,28 @@ export function buildPreview(rows: () => ComboLogic[]): PreviewHandle {
|
||||
|
||||
const redraw = async (): Promise<void> => {
|
||||
const blocks: HTMLElement[] = [];
|
||||
for (const row of rows()) {
|
||||
inputs.clear();
|
||||
for (const row of combo().담은로직) {
|
||||
try {
|
||||
const one = await fetchLogic(row.키);
|
||||
const one = await fetchLogic(row.로직);
|
||||
inputs.set(row.로직, one.logic.입력);
|
||||
blocks.push(
|
||||
el("div", {
|
||||
className: "m01lab__info",
|
||||
children: [
|
||||
el("strong", {
|
||||
className: "m01lab__cell--wide",
|
||||
text: `${row.키} ${one.logic.이름}`,
|
||||
text: `${row.로직} ${one.logic.이름}`,
|
||||
}),
|
||||
...one.logic.입력.map((i) => inputCell(i, row)),
|
||||
...one.logic.입력.map((i) => inputCell(i, row.로직, values)),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* 못 읽은 로직은 계산 때 까닭이 뜸 */
|
||||
/* 못 읽은 로직은 미리 보기에서 그 줄만 까닭이 뜸 */
|
||||
}
|
||||
}
|
||||
inputs.replaceChildren(...blocks);
|
||||
form.replaceChildren(...blocks);
|
||||
out.replaceChildren(el("p", { className: "m01-logic__muted", text: tc("Prev_Wait") }));
|
||||
};
|
||||
|
||||
@@ -139,7 +156,7 @@ export function buildPreview(rows: () => ComboLogic[]): PreviewHandle {
|
||||
children: [el("h3", { text: tc("Prev_Title") })],
|
||||
}),
|
||||
el("h4", { text: tc("Prev_Inputs") }),
|
||||
inputs,
|
||||
form,
|
||||
button,
|
||||
out,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user