feat(M02): 일위대가 수량산출 열 — 위 결과단위 기준 수량 · 아래 지금 수량 · 누르면 손 입력 / 엑셀에서 고르기(면적 칸 연결 · [저장] 때 면적 문서 함께) · 버튼 공용 양식(40-1 ④ ⑤)

- Labor_Rows 수량산출 칸(qtyCell) · 결과 단위 왼쪽 · 좁은 칸은 윗줄
- Labor_Input 수량 칸에 [엑셀에서 고르기](sub3 pickAreaCell) · 이으면 손 수량 지움 · 모달 닫음
- Labor_Detail: 면적 문서 · 판을 들고 linkAreaCell · recalcWorkbook · 저장 안 한 연결은 calc 에 칸 값으로 · saveArea
- UI_Main labor 갈래: [저장] = 면적 문서 먼저 → 일위대가 판 다시 읽음 → 일위대가(서버가 면적 저장 때 일위대가 계산값을 다시 적어 판이 바뀜)
- 로직 추가 · 자재 추가 · 올리기 · 내리기 · 빼기 = 기본 크기 버튼(sm 알약 꼴 아님)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
2026-10-04 22:39:46 +09:00
co-authored by Claude Opus 5.5
parent e70fc184ae
commit 35ccc26afa
6 changed files with 219 additions and 16 deletions
@@ -33,6 +33,8 @@ export interface QtySeat {
linked: string | null;
/** 손 입력 단위 표기(분모 · 자재 단위) */
unit: string;
/** [엑셀에서 고르기](40-1) — 그 열 면적산출 칸을 골라 이음 · 이었으면 참(모달 닫음) · 없으면 단추 없음 */
pick?: (() => Promise<boolean>) | null;
}
interface QtyPart {
@@ -42,7 +44,12 @@ interface QtyPart {
}
/** 수량 칸 — 「면적산출 칸」 / 「손 입력」 · 손이 칸보다 앞섬 */
function qtyPart(seat: QtySeat, readOnly: boolean, goArea: (() => void) | null): QtyPart {
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;
@@ -67,6 +74,16 @@ function qtyPart(seat: QtySeat, readOnly: boolean, goArea: (() => void) | null):
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",
@@ -83,6 +100,7 @@ function qtyPart(seat: QtySeat, readOnly: boolean, goArea: (() => void) | null):
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 })),
@@ -174,7 +192,7 @@ 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));
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();
@@ -226,7 +244,7 @@ export function openQtyModal(
closeLabel: tc("In_Close"),
dialogClass: "m02-modal m02-labin",
mount: (body, close) => {
const qty = qtyPart(seat, readOnly, goArea && leaveTo(goArea, 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");