feat(M02): 일위대가 수량산출 모달 하나 — 지금 수량 · 손 입력 · 연결 풀기 + 면적산출 시트를 모달 안에 바로(칸 누름 = 이음) · 넣을 값 모달에서 수량 칸 뺌(43-1 · 43-2)
- 새 Labor_QtyModal.ts(로직 줄 수량산출 칸 · 자재 줄 같음 · [확인] = 손 · 고른 칸 · 풀기 · [닫기] = 그대로) · 두 단계 모달([엑셀에서 고르기]) 없앰 - AreaCellPick: 시트 부분을 mountAreaSheet 로 뗌(읽기만 · 실제 값 · 이은 칸 색 · 함께 잇기 · 바꾸기 물음 그대로) · 모달 pickAreaCell 지움 - Labor_Detail: openQty · openMat → openQtyFor · linkCell · Labor_Input: 수량 칸 · openQtyModal 지움(입력값 · 메모만) - 화면 판정 시나리오 resources/tester/ui_checks/43-1_수량산출_모달.js · 시험 test_m02_area_panel 부품 기대값 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Labor_Input.ts
|
||||
* 일위대가 입력 모달(39-3) — 담은 로직 줄을 누르면 그 로직 입력 전부(빈 칸 = 자동값) · 메모 · 수량
|
||||
* (면적산출 이은 칸 · 값 보임 / 손 입력 / 「면적산출에서 잇기」). 자재 줄은 수량만(`openQtyModal`).
|
||||
* 일위대가 입력 모달(39-3) — 단가산출 로직 줄을 누르면 그 로직 입력 전부(빈 칸 = 자동값) · 메모.
|
||||
* 수량은 이 모달에 없음(43-2) — 수량산출 칸 누름 = `Labor_QtyModal`(면적산출 시트를 모달 안에).
|
||||
* [확인] = 줄 값만 바꿈(화면 캐시) · 저장은 상세 [저장]. 값 칸 규칙 = `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";
|
||||
@@ -20,102 +19,6 @@ const fill = (text: string, value: string): string => text.replace("{value}", va
|
||||
/** 면적산출 상세로 감 — 옮겨 갔으면 참(물음에서 취소 = 거짓) */
|
||||
export type GoArea = () => Promise<boolean>;
|
||||
|
||||
/** 옮겨 갔을 때만 모달을 닫음 — 취소면 모달 · 넣던 값 그대로 */
|
||||
const leaveTo =
|
||||
(go: GoArea, close: () => void): (() => void) =>
|
||||
() =>
|
||||
void go().then((moved) => moved && close());
|
||||
|
||||
export interface QtySeat {
|
||||
/** 지금 수량 */
|
||||
qty: LaborQty;
|
||||
/** 면적산출 이은 칸 글(「B6 = 12.5 ㎡」) — 없으면 null */
|
||||
linked: string | null;
|
||||
/** 손 입력 단위 표기(분모 · 자재 단위) */
|
||||
unit: string;
|
||||
/** [엑셀에서 고르기](40-1) — 그 열 면적산출 칸을 골라 이음 · 이었으면 참(모달 닫음) · 없으면 단추 없음 */
|
||||
pick?: (() => Promise<boolean>) | null;
|
||||
}
|
||||
|
||||
interface QtyPart {
|
||||
root: HTMLElement;
|
||||
/** 확인 때 값 — 손 입력이 수가 아니면 undefined(막음) */
|
||||
read: () => LaborQty | undefined;
|
||||
}
|
||||
|
||||
/** 수량 칸 — 「면적산출 칸」 / 「손 입력」 · 손이 칸보다 앞섬 */
|
||||
function qtyPart(
|
||||
seat: QtySeat,
|
||||
readOnly: boolean,
|
||||
goArea: (() => void) | null,
|
||||
close: () => void,
|
||||
): 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 pickFn = seat.pick;
|
||||
const pick =
|
||||
pickFn && !readOnly
|
||||
? createButton({
|
||||
label: tc("In_QtyPick"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => void pickFn().then((linked) => linked && close()),
|
||||
})
|
||||
: 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"),
|
||||
}),
|
||||
...(pick ? [pick] : []),
|
||||
...(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",
|
||||
@@ -131,20 +34,16 @@ function footer(readOnly: boolean, ok: () => void, close: () => void): HTMLEleme
|
||||
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: GoArea | null;
|
||||
done: (next: LaborInputResult) => void;
|
||||
}
|
||||
|
||||
@@ -192,12 +91,9 @@ export function openLaborInput(opt: LaborInputOptions): void {
|
||||
}) as HTMLInputElement;
|
||||
memo.value = opt.memo ?? "";
|
||||
memo.readOnly = opt.readOnly;
|
||||
const qty = qtyPart(opt.seat, opt.readOnly, opt.goArea && leaveTo(opt.goArea, close), close);
|
||||
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, 수량 });
|
||||
opt.done({ 입력값: { ...draft }, 메모: memo.value.trim() || null });
|
||||
close();
|
||||
};
|
||||
body.append(
|
||||
@@ -222,7 +118,6 @@ export function openLaborInput(opt: LaborInputOptions): void {
|
||||
memo,
|
||||
],
|
||||
}),
|
||||
qty.root,
|
||||
],
|
||||
}),
|
||||
footer(opt.readOnly, ok, close),
|
||||
@@ -230,28 +125,3 @@ export function openLaborInput(opt: LaborInputOptions): void {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 자재 줄 수량 모달 */
|
||||
export function openQtyModal(
|
||||
title: string,
|
||||
seat: QtySeat,
|
||||
readOnly: boolean,
|
||||
goArea: GoArea | 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 && leaveTo(goArea, close), close);
|
||||
const ok = (): void => {
|
||||
const 수량 = qty.read();
|
||||
if (수량 === undefined) return showToast(tc("In_QtyNeed"), "error");
|
||||
done(수량);
|
||||
close();
|
||||
};
|
||||
body.append(qty.root, footer(readOnly, ok, close));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user