feat(M02-일위대가조합): 조합 다이어그램 - 로직별 넣을 값 → 로직 카드 → 비용 구분 칸 · 미리 보기와 값 한 벌 · 카드 누르면 단가산출 로직 화면
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
previewCombo,
|
||||
type Combo,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { buildDiagramData, type DiagramData } from "./M02_MasterTemplete_Labor_Diagram";
|
||||
import { tc, type ComboTextKey } from "./M02_MasterTemplete_Labor_Text";
|
||||
|
||||
const COSTS = ["노무비", "재료비", "경비"] as const;
|
||||
@@ -35,6 +36,7 @@ function inputCell(
|
||||
key: string,
|
||||
values: Record<string, Record<string, string>>,
|
||||
onEdit: () => void,
|
||||
onInput: () => void,
|
||||
): HTMLElement {
|
||||
const mine = (values[key] ??= {});
|
||||
const value = mine[input.이름] ?? initial(input);
|
||||
@@ -48,6 +50,7 @@ function inputCell(
|
||||
control.setAttribute("data-input", `${key}|${input.이름}`);
|
||||
const edit = (): void => {
|
||||
mine[input.이름] = control.value;
|
||||
onInput();
|
||||
};
|
||||
control.addEventListener("input", edit);
|
||||
control.addEventListener("change", () => {
|
||||
@@ -61,6 +64,12 @@ export interface PreviewHandle {
|
||||
root: HTMLElement;
|
||||
/** 담은 로직이 바뀐 뒤 입력 칸을 다시 그림 */
|
||||
redraw: () => Promise<void>;
|
||||
/** 다이어그램이 쓰는 값 한 벌 — 넣은 값 · 로직 이름 · 결과(저장 안 함) */
|
||||
data: () => DiagramData;
|
||||
/** 입력 칸이 바뀌거나 결과가 나올 때마다 부름(돌려받은 함수 = 그만 받기) */
|
||||
onUpdate: (fn: () => void) => () => void;
|
||||
/** 그 입력 칸으로 가서 깜빡임 */
|
||||
focusInput: (key: string, name: string) => void;
|
||||
}
|
||||
|
||||
export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
@@ -69,6 +78,11 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
const inputs = new Map<string, LogicInput[]>();
|
||||
/** 로직마다 자동값은 한 번만 받음 */
|
||||
const asked = new Set<string>();
|
||||
/** 다이어그램에 넘길 것 — 로직 이름 · 결과 단위 · 마지막 미리 보기 줄 */
|
||||
const meta = new Map<string, { name: string; unit: string }>();
|
||||
let lines: Parameters<typeof buildDiagramData>[0]["lines"] = null;
|
||||
const listeners = new Set<() => void>();
|
||||
const notify = (): void => listeners.forEach((f) => f());
|
||||
const form = el("div", { className: "m01-logic__stack" });
|
||||
const out = el("div", {
|
||||
className: "m01-master__grid-wrap",
|
||||
@@ -85,6 +99,8 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
|
||||
const run = async (): Promise<void> => {
|
||||
out.replaceChildren(el("p", { className: "m01-logic__muted", text: "…" }));
|
||||
lines = null;
|
||||
notify();
|
||||
const send: Record<string, Record<string, unknown>> = {};
|
||||
for (const [key, defs] of inputs) {
|
||||
send[key] = {};
|
||||
@@ -97,6 +113,8 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
let got;
|
||||
try {
|
||||
got = await previewCombo(combo(), send);
|
||||
lines = got.줄;
|
||||
notify();
|
||||
} catch (error) {
|
||||
out.replaceChildren(
|
||||
el("p", { text: `${tc("Prev_Fail")}: ${error instanceof Error ? error.message : ""}` }),
|
||||
@@ -160,6 +178,7 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
mine[i.이름] = String(got);
|
||||
}
|
||||
inputs.set(row.로직, one.logic.입력);
|
||||
meta.set(row.로직, { name: one.logic.이름, unit: one.logic.결과단위 ?? "" });
|
||||
blocks.push(
|
||||
el("div", {
|
||||
className: "m01lab__info",
|
||||
@@ -168,7 +187,9 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
className: "m01lab__cell--wide",
|
||||
text: `${row.로직} ${one.logic.이름}`,
|
||||
}),
|
||||
...one.logic.입력.map((i) => inputCell(i, row.로직, values, () => void run())),
|
||||
...one.logic.입력.map((i) =>
|
||||
inputCell(i, row.로직, values, () => void run(), notify),
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -177,6 +198,7 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
}
|
||||
}
|
||||
form.replaceChildren(...blocks);
|
||||
notify();
|
||||
if (blocks.length)
|
||||
await run(); // 열자마자 한 번 미리 보기 계산
|
||||
else out.replaceChildren(el("p", { className: "m01-logic__muted", text: tc("Prev_Wait") }));
|
||||
@@ -195,5 +217,28 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
out,
|
||||
],
|
||||
});
|
||||
return { root, redraw };
|
||||
const data = (): DiagramData =>
|
||||
buildDiagramData({
|
||||
rows: combo().담은로직.map((r) => ({
|
||||
key: r.로직,
|
||||
name: meta.get(r.로직)?.name ?? r.로직,
|
||||
unit: meta.get(r.로직)?.unit ?? "",
|
||||
inputs: (inputs.get(r.로직) ?? []).map((i) => ({ name: i.이름, unit: i.단위 ?? "" })),
|
||||
})),
|
||||
values,
|
||||
lines,
|
||||
});
|
||||
const focusInput = (key: string, name: string): void => {
|
||||
const box = root.querySelector<HTMLElement>(`[data-input="${CSS.escape(`${key}|${name}`)}"]`);
|
||||
if (!box) return;
|
||||
box.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
box.focus({ preventScroll: true });
|
||||
box.classList.add("m02-labor__flash");
|
||||
window.setTimeout(() => box.classList.remove("m02-labor__flash"), 1600);
|
||||
};
|
||||
const onUpdate = (fn: () => void): (() => void) => {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
};
|
||||
return { root, redraw, data, onUpdate, focusInput };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user