Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts
T
eomsangdonandClaude Opus 5.5 a1673aeb10 feat(M02): 서고 사전조합 상세 줄 넣기 — [추가] · 수량 손 입력 · 자기 참조 422(51-6)
- 사전조합 상세 머리 [추가] = 고르기 창 서고 줄만(단가승격 · 사전조합 · 위 층만 · 구분 거름 없음)
- 새 줄 수량 기본 손 1 · 수량 창은 손 입력만(면적산출 시트 없음) · 위로 · 아래로 · 삭제 · [저장]
- 서버: 사전조합이 자기 자신을 담으면 422 「순환 — 자기 자신」 · 단가승격 상세는 한 줄 고정 그대로
- 시험 한 개 · 화면 시나리오 51-6(가짜 응답 · 서고 쓰기 없음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
2026-10-05 19:42:39 +09:00

176 lines
6.7 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Labor_QtyModal.ts
* 일위대가 수량산출 모달(43-1) — 수량산출 칸 누름(로직 줄 · 자재 줄 같음) = 모달 하나.
* · 위 = 지금 수량(손 · 칸 · —) · 손 입력 칸 · [연결 풀기]
* · 아래 = 그 열 면적산출 시트를 모달 안에 바로(읽기만 · 실제 값 · 이은 칸 색 · `mountAreaSheet`) ·
* 칸 누름 = 그 칸을 고름(함께 잇기 · 바꾸기 물음 그대로)
* · [확인] = 손 값이 있으면 손 · 아니면 고른 칸으로 잇기 · 풀기 · [닫기] = 그대로
* 잇기 · 풀기는 화면 문서만(저장은 일위대가 [저장] 때 면적 문서와 함께 · `Labor_Detail`).
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { openModal } from "@ui/ui_template_modal";
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
import { mountAreaSheet, type AreaCellPick } from "./M02_MasterTemplete_AreaCellPick";
import type { GoArea } from "./M02_MasterTemplete_Labor_Input";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import "./M02_Labor_Values.css";
import "./M02_MasterTemplete_Labor.css";
/** 모달 답 — 손 · 칸 따름(손 지움) · 고른 칸으로 잇기 · 연결 풀기 · 그대로 */
export type QtyResult =
| { kind: "hand"; 손: number }
| { kind: "follow" }
| { kind: "cell"; pick: AreaCellPick }
| { kind: "unlink" }
| { kind: "keep" };
export interface QtyModalOptions {
/** 「GF000219 돌쌓기 메쌓기(인력)」 */
title: string;
/** 지금 수량 글(「손 5 ㎥」 · 「B6 = 12.5」 · 「—」) */
now: string;
/** 지금 칸에 이어져 있나 — [연결 풀기] */
linked: boolean;
/** 지금 손 값(없으면 null) */
hand: number | null;
/** 손 입력 단위(분모 · 자재 단위) */
unit: string;
readOnly: boolean;
/** 그 열 면적산출 — `pill` 이 없으면(계산 전) 시트 대신 안내 */
sheet: { doc: Workbook | null; layer: Layer; column: string; pill: string | null };
/** [면적산출 상세로] — 없으면 단추 없음 */
goArea: GoArea | null;
/** 손 입력만(서고 사전조합 줄 · 51-6) — 면적산출 시트 없음 */
handOnly?: boolean;
done: (result: QtyResult) => void;
}
export function openQtyModal(opt: QtyModalOptions): void {
let pending: QtyResult = { kind: "keep" };
let sheet: { destroy: () => void } | null = null;
openModal({
title: `${tc("Labor_QtyCol")} — ${opt.title}`,
closeLabel: tc("In_Close"),
dialogClass: "m02-modal m02-labin m02-labqty",
beforeClose: () => (sheet?.destroy(), true),
mount: (body, closeNow) => {
const finish = (): void => {
sheet?.destroy();
closeNow();
};
const now = el("span", {
className: "m02-labqty__now",
text: opt.now,
attrs: { "data-labor-qty-now": "" },
});
const state = el("small", {
className: "m02-labin__muted",
attrs: { "data-labor-qty-state": "" },
});
const hand = el("input", {
className: "ui-input m02-inputval__control",
attrs: {
type: "number",
step: "any",
"data-labor-qty": "hand",
"aria-label": tc("In_QtyHand"),
},
}) as HTMLInputElement;
hand.readOnly = opt.readOnly;
if (opt.hand !== null) hand.value = String(opt.hand);
const unlink =
opt.linked && !opt.readOnly
? createButton({
label: tc("Labor_Unlink"),
variant: "ghost",
onClick: () => {
pending = { kind: "unlink" };
hand.value = "";
state.textContent = tc("Labor_UnlinkPending");
},
})
: null;
const go = opt.goArea
? createButton({
label: tc("In_QtyGoArea"),
variant: "ghost",
onClick: () =>
void opt.goArea?.().then((moved) => {
if (moved) finish();
}),
})
: null;
const host = el("div", {
className: "m02-cellpick__sheet",
attrs: { "data-labor-qty-sheet": "" },
});
if (opt.handOnly) {
// 시트 없음
} else if (opt.sheet.pill) {
sheet = mountAreaSheet(host, {
doc: opt.sheet.doc,
layer: opt.sheet.layer,
column: opt.sheet.column,
pill: opt.sheet.pill,
onPick: (pick) => {
if (opt.readOnly) return;
pending = { kind: "cell", pick };
hand.value = ""; // 칸을 고르면 손 값은 지움(칸을 따름)
state.textContent = tc("Labor_CellPending").replace("{value}", pick.칸);
},
});
} else
host.replaceChildren(
el("p", { className: "m02-labin__muted", text: tc("Labor_PickWait") }),
);
hand.addEventListener("input", () => {
if (hand.value.trim() !== "") state.textContent = "";
});
const ok = (): void => {
const raw = hand.value.trim();
if (raw !== "") {
const v = Number(raw);
if (!Number.isFinite(v)) return showToast(tc("In_QtyNeed"), "error");
opt.done({ kind: "hand", 손: v });
} else if (pending.kind !== "keep") opt.done(pending);
else if (opt.hand !== null) opt.done({ kind: "follow" }); // 손 값을 지움 = 칸을 따름
finish();
};
body.append(
el("div", {
className: "m02-labqty__head",
children: [
el("span", { className: "m02-inputval__name", text: tc("Labor_QtyNow") }),
now,
el("label", {
className: "m02-labin__qty-line",
children: [
el("span", { text: tc("In_QtyHand") }),
hand,
el("small", { text: opt.unit }),
],
}),
...(unlink ? [unlink] : []),
...(go ? [go] : []),
],
}),
state,
...(opt.handOnly
? []
: [el("p", { className: "m02-cellpick__hint", text: tc("Labor_SheetHint") }), host]),
el("div", {
className: "m02-labin__foot",
children: opt.readOnly
? [createButton({ label: tc("In_Close"), variant: "ghost", onClick: finish })]
: [
createButton({ label: tc("In_Close"), variant: "ghost", onClick: finish }),
createButton({ label: tc("In_Ok"), variant: "filled", onClick: ok }),
],
}),
);
},
});
}