- 새 Labor_Detail.ts(mountLaborDetail · 차례 = 정보 → 담은 로직 → 자재목록 → 다이어그램 · 고칠 때마다 calc 를 뒤에서 · 최신 답만 · 같은 로직 · 자재 두 번 막음) - 새 Labor_Input.ts(담은 로직 줄 입력 모달 · 빈 칸 = 자동값 · 메모 · 수량 = 이은 칸 / 손 입력 / 면적산출에서 잇기 · 사라진 입력값 알림 · 자재 수량 모달) - 새 Labor_Rows.ts(작은 글 「지역 서울 · 수량 B6 = 12.5㎡」 · 빈 값 n 붉게 · 금액 칸 넷 두 줄(위 기준 · 아래 반영) · 합계 줄 · 보기 갈래는 손잡이 없이) - Labor_Api.ts: labor 문서 · calc 답 모양 · calcLabor · fetchLogicInputs · previewCombo 지움 - Labor_Diagram.ts: buildDiagramData → buildLaborDiagram(줄 입력값 + calc · 수량 있으면 반영 · 없으면 기준 표기 · 자재목록 카드 하나 → 재료비 · 알약 = 입력 모달) - 미리 보기 지움: Labor_Preview.ts · M02 preview 길 · Combo_Store.preview · 옛 조합 상세의 미리 보기 · 다이어그램 칸 - 시험: test_m02_labor_diagram 새 모양 · test_m01_combo 미리 보기 시험은 같은 계산(combo_preview)을 바로 부름 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
242 lines
8.3 KiB
TypeScript
242 lines
8.3 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor_Input.ts
|
|
* 일위대가 입력 모달(39-3) — 담은 로직 줄을 누르면 그 로직 입력 전부(빈 칸 = 자동값) · 메모 · 수량
|
|
* (면적산출 이은 칸 · 값 보임 / 손 입력 / 「면적산출에서 잇기」). 자재 줄은 수량만(`openQtyModal`).
|
|
* [확인] = 줄 값만 바꿈(화면 캐시) · 저장은 상세 [저장]. 값 칸 규칙 = `M02_Labor_Values`(한 벌).
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import type { ItemInput, ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
|
|
import type { LaborQty } from "./M02_MasterTemplete_Labor_Api";
|
|
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
|
import { createValueControl } from "./M02_Labor_Values";
|
|
import { keepKnown, valueHint, type LaborValues } from "./M02_Labor_Values_Model";
|
|
import "./M02_Labor_Values.css";
|
|
import "./M02_MasterTemplete_Labor.css";
|
|
|
|
const fill = (text: string, value: string): string => text.replace("{value}", value);
|
|
|
|
export interface QtySeat {
|
|
/** 지금 수량 */
|
|
qty: LaborQty;
|
|
/** 면적산출 이은 칸 글(「B6 = 12.5 ㎡」) — 없으면 null */
|
|
linked: string | null;
|
|
/** 손 입력 단위 표기(분모 · 자재 단위) */
|
|
unit: string;
|
|
}
|
|
|
|
interface QtyPart {
|
|
root: HTMLElement;
|
|
/** 확인 때 값 — 손 입력이 수가 아니면 undefined(막음) */
|
|
read: () => LaborQty | undefined;
|
|
}
|
|
|
|
/** 수량 칸 — 「면적산출 칸」 / 「손 입력」 · 손이 칸보다 앞섬 */
|
|
function qtyPart(seat: QtySeat, readOnly: boolean, goArea: (() => void) | null): QtyPart {
|
|
const name = `m02-labin-qty-${Math.random().toString(36).slice(2)}`;
|
|
const radio = (value: "cell" | "hand"): HTMLInputElement => {
|
|
const input = el("input", { attrs: { type: "radio", name, value } }) as HTMLInputElement;
|
|
input.disabled = readOnly;
|
|
return input;
|
|
};
|
|
const cell = radio("cell");
|
|
const hand = radio("hand");
|
|
const number = el("input", {
|
|
className: "ui-input m02-inputval__control",
|
|
attrs: { type: "number", step: "any", "data-labor-qty": "hand" },
|
|
}) as HTMLInputElement;
|
|
number.readOnly = readOnly;
|
|
if (seat.qty) number.value = String(seat.qty.손);
|
|
(seat.qty ? hand : cell).checked = true;
|
|
number.disabled = !hand.checked;
|
|
for (const r of [cell, hand])
|
|
r.addEventListener("change", () => {
|
|
number.disabled = !hand.checked;
|
|
if (hand.checked) number.focus();
|
|
});
|
|
const go = goArea
|
|
? createButton({ label: tc("In_QtyGoArea"), variant: "ghost", size: "sm", onClick: goArea })
|
|
: null;
|
|
const line = (input: HTMLInputElement, label: string, ...rest: HTMLElement[]): HTMLElement =>
|
|
el("label", {
|
|
className: "m02-labin__qty-line",
|
|
children: [input, el("span", { text: label }), ...rest],
|
|
});
|
|
const root = el("section", {
|
|
className: "m02-labin__qty",
|
|
children: [
|
|
el("h4", { className: "m02-inputval__logic-name", text: tc("In_Qty") }),
|
|
line(
|
|
cell,
|
|
tc("In_QtyCell"),
|
|
el("small", {
|
|
className: "m02-labin__muted",
|
|
text: seat.linked ? fill(tc("In_QtyLinked"), seat.linked) : tc("In_QtyNoLink"),
|
|
}),
|
|
...(go ? [go] : []),
|
|
),
|
|
line(hand, tc("In_QtyHand"), number, el("small", { text: seat.unit })),
|
|
],
|
|
});
|
|
return {
|
|
root,
|
|
read: () => {
|
|
if (!hand.checked) return null;
|
|
const v = Number(number.value);
|
|
return number.value.trim() !== "" && Number.isFinite(v) ? { 손: v } : undefined;
|
|
},
|
|
};
|
|
}
|
|
|
|
function footer(readOnly: boolean, ok: () => void, close: () => void): HTMLElement {
|
|
return el("div", {
|
|
className: "m02-labin__foot",
|
|
children: readOnly
|
|
? [createButton({ label: tc("In_Close"), variant: "ghost", onClick: close })]
|
|
: [
|
|
createButton({ label: tc("In_Cancel"), variant: "ghost", onClick: close }),
|
|
createButton({ label: tc("In_Ok"), variant: "filled", onClick: ok }),
|
|
],
|
|
});
|
|
}
|
|
|
|
export interface LaborInputResult {
|
|
입력값: LaborValues;
|
|
메모: string | null;
|
|
수량: LaborQty;
|
|
}
|
|
|
|
export interface LaborInputOptions {
|
|
/** 「GF000219 메쌓기」 */
|
|
title: string;
|
|
/** 로직 입력 목록(없으면 = 못 읽음 · 넣을 값 없이 수량만) */
|
|
logic: ItemLogic | null;
|
|
values: LaborValues;
|
|
memo: string | null;
|
|
seat: QtySeat;
|
|
readOnly: boolean;
|
|
/** 「면적산출에서 잇기」 — 모달을 닫고 면적산출 상세로(없으면 단추 없음) */
|
|
goArea: (() => void) | null;
|
|
done: (next: LaborInputResult) => void;
|
|
}
|
|
|
|
/** 담은 로직 줄 입력 모달 */
|
|
export function openLaborInput(opt: LaborInputOptions): void {
|
|
const inputs: ItemInput[] = opt.logic?.입력 ?? [];
|
|
const { kept, lost } = keepKnown(inputs, opt.values);
|
|
const draft: LaborValues = { ...kept };
|
|
const bad = new Set<string>();
|
|
openModal({
|
|
title: fill(tc("In_Title"), opt.title),
|
|
closeLabel: tc("In_Close"),
|
|
dialogClass: "m02-modal m02-labin",
|
|
mount: (body, close) => {
|
|
const rows = inputs.map((input) => {
|
|
const field = createValueControl(input, draft[input.이름] ?? null, (value, isBad) => {
|
|
if (isBad) bad.add(input.이름);
|
|
else bad.delete(input.이름);
|
|
if (value === null) delete draft[input.이름];
|
|
else draft[input.이름] = value;
|
|
});
|
|
field.control.disabled = opt.readOnly;
|
|
field.control.setAttribute("data-labor-input", input.이름);
|
|
const hint = valueHint(input);
|
|
return el("div", {
|
|
className: "m02-inputval__row",
|
|
children: [
|
|
el("span", {
|
|
className: "m02-inputval__name",
|
|
text: input.단위 ? `${input.이름} (${input.단위})` : input.이름,
|
|
attrs: { title: [input.설명, hint].filter(Boolean).join(" · ") },
|
|
}),
|
|
el("div", { className: "m02-inputval__cell", children: [field.control, field.error] }),
|
|
],
|
|
});
|
|
});
|
|
const memo = el("input", {
|
|
className: "ui-input m02-inputval__control",
|
|
attrs: { type: "text", maxlength: "200", "data-labor-memo": "" },
|
|
}) as HTMLInputElement;
|
|
memo.value = opt.memo ?? "";
|
|
memo.readOnly = opt.readOnly;
|
|
const qty = qtyPart(
|
|
opt.seat,
|
|
opt.readOnly,
|
|
opt.goArea &&
|
|
(() => {
|
|
close();
|
|
opt.goArea?.();
|
|
}),
|
|
);
|
|
const ok = (): void => {
|
|
if (bad.size) return showToast(tc("In_Bad"), "error");
|
|
const 수량 = qty.read();
|
|
if (수량 === undefined) return showToast(tc("In_QtyNeed"), "error");
|
|
opt.done({ 입력값: { ...draft }, 메모: memo.value.trim() || null, 수량 });
|
|
close();
|
|
};
|
|
body.append(
|
|
el("div", {
|
|
className: "m02-inputval",
|
|
children: [
|
|
...(lost.length
|
|
? [
|
|
el("p", {
|
|
className: "m02-labin__lost",
|
|
text: fill(tc("Labor_Lost"), lost.join(", ")),
|
|
}),
|
|
]
|
|
: []),
|
|
...(rows.length
|
|
? rows
|
|
: [el("p", { className: "m02-labin__muted", text: tc("In_None") })]),
|
|
el("div", {
|
|
className: "m02-inputval__row",
|
|
children: [
|
|
el("span", { className: "m02-inputval__name", text: tc("In_Memo") }),
|
|
memo,
|
|
],
|
|
}),
|
|
qty.root,
|
|
],
|
|
}),
|
|
footer(opt.readOnly, ok, close),
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
/** 자재 줄 수량 모달 */
|
|
export function openQtyModal(
|
|
title: string,
|
|
seat: QtySeat,
|
|
readOnly: boolean,
|
|
goArea: (() => void) | null,
|
|
done: (qty: LaborQty) => void,
|
|
): void {
|
|
openModal({
|
|
title,
|
|
closeLabel: tc("In_Close"),
|
|
dialogClass: "m02-modal m02-labin",
|
|
mount: (body, close) => {
|
|
const qty = qtyPart(
|
|
seat,
|
|
readOnly,
|
|
goArea &&
|
|
(() => {
|
|
close();
|
|
goArea();
|
|
}),
|
|
);
|
|
const ok = (): void => {
|
|
const 수량 = qty.read();
|
|
if (수량 === undefined) return showToast(tc("In_QtyNeed"), "error");
|
|
done(수량);
|
|
close();
|
|
};
|
|
body.append(qty.root, footer(readOnly, ok, close));
|
|
},
|
|
});
|
|
}
|