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:
2026-10-05 00:35:03 +09:00
co-authored by Claude Opus 5.5
parent 31a753b7fd
commit ef24de8b63
8 changed files with 474 additions and 325 deletions
@@ -19,7 +19,7 @@ import {
type ItemLogic,
type Layer,
} from "./M02_MasterTemplete_Api_Fetch";
import { linkAreaCell, pickAreaCell } from "./M02_MasterTemplete_AreaCellPick";
import { linkAreaCell } from "./M02_MasterTemplete_AreaCellPick";
import { settle } from "./M02_MasterTemplete_Labor_Calc";
import {
areaLinks,
@@ -28,6 +28,7 @@ import {
normalizeLabor,
type LaborCalc,
type LaborDoc,
type LaborQty,
type LaborRow,
} from "./M02_MasterTemplete_Labor_Api";
import {
@@ -35,9 +36,10 @@ import {
diagramTitle,
renderComboDiagram,
} from "./M02_MasterTemplete_Labor_Diagram";
import { openLaborInput, openQtyModal, type GoArea } from "./M02_MasterTemplete_Labor_Input";
import { openLaborInput, type GoArea } from "./M02_MasterTemplete_Labor_Input";
import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal";
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows";
import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { keepKnown } from "./M02_Labor_Values_Model";
import { pickMaterial } from "./M02_Material_Pick";
@@ -174,37 +176,22 @@ export function mountLaborDetail(
} else recalc();
};
/** 이은 칸 글 「B6 = 12.5 ㎡」 — 알약이 칸에 안 이어졌으면 null */
const linkedText = (pill: string | null | undefined, unit: string): string | null => {
const at = pill ? links.get(pill) : undefined;
if (!at) return null;
return `${at.칸} = ${at.값 === null ? "—" : at.값.toLocaleString("ko-KR")}${unit ? ` ${unit}` : ""}`;
};
/** [엑셀에서 고르기] — 그 열 면적산출 칸을 골라 그 알약을 이음(화면 문서 · 저장은 [저장]) · 이었으면 참 */
const pickCell = async (pill: string | null | undefined, label: string): Promise<boolean> => {
if (!pill || !area) {
showToast(tc(area ? "Labor_PickWait" : "Labor_NoArea"), "error");
/** 칸 잇기 · 풀기(`cell` null) — 화면 문서만(저장은 [저장]) · 됐으면 참 */
const linkCell = (pill: string, cell: string | null, replace = false): boolean => {
if (!area) {
showToast(tc("Labor_NoArea"), "error");
return false;
}
const got = await pickAreaCell({
doc: area.doc,
layer: opt.layer,
column: opt.column,
pill,
label,
});
if (!got || !alive) return false;
const info = pills.get(pill);
const next = linkAreaCell(
area.doc,
pill,
got.칸,
cell,
{
...(info?.이름 ? { 이름: info.이름 } : {}),
...(info?.단위 ? { 단위: info.단위 } : {}),
},
got.바꾸기 === true, // 그 칸 다른 알약 — 기본 함께 · [바꾸기]면 뺌(물음은 고르기 모달 안)
replace, // 그 칸 다른 알약 — 기본 함께 · [바꾸기]면 뺌(물음은 시트 안)
);
if (!next) {
showToast(tc("Labor_BadCell"), "error");
@@ -217,6 +204,37 @@ export function mountLaborDetail(
return true;
};
/** 수량산출 모달(43-1) — 로직 줄 · 자재 줄 같음 · 손 · 칸 잇기 · 풀기로 `target.수량` 을 바꿈 */
const openQtyFor = (
title: string,
target: { 수량: LaborQty },
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
unit: string,
): void => {
const pill = line?.알약 ?? null;
openQtyModal({
title,
now: qtyShort(line, unit, cells()),
linked: !!pill && links.has(pill),
hand: target.수량?.손 ?? null,
unit,
readOnly: !edit,
sheet: { doc: area?.doc ?? null, layer: opt.layer, column: opt.column, pill },
goArea: opt.goArea,
done: (r) => {
if (r.kind === "keep") return;
if (r.kind === "hand") target.수량 = { 손: r.손 };
else if (r.kind === "follow") target.수량 = null;
else {
const cell = r.kind === "cell" ? r.pick.칸 : null;
if (!pill || !linkCell(pill, cell, r.kind === "cell" && r.pick.바꾸기 === true)) return;
target.수량 = null; // 칸을 따름
}
changed(true);
},
});
};
/** 아직 안 받은 로직 입력 목록만 받음 */
const loadLogics = async (): Promise<void> => {
const want = cur.담은로직.map((r) => r.로직).filter((k) => !logics.has(k));
@@ -247,85 +265,38 @@ export function mountLaborDetail(
function openRow(i: number): void {
const row: LaborRow = cur.담은로직[i];
const logic = logics.get(row.로직) ?? null;
const line = lineOf(row.로직);
openLaborInput({
title: `${row.로직} ${logic?.이름 ?? ""}`.trim(),
logic,
values: row.입력값,
memo: row.메모,
seat: {
qty: row.수량,
linked: linkedText(line?.알약, line?.분모 ?? ""),
unit: line?.분모 ?? "",
pick: () => pickRow(i),
},
readOnly: !edit,
goArea: opt.goArea,
done: (next) => {
const values = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값;
const qtyOnly = JSON.stringify(values) === JSON.stringify(row.입력값);
const same = JSON.stringify(values) === JSON.stringify(row.입력값);
row.입력값 = values;
row.메모 = next.메모;
row.수량 = next.수량;
changed(qtyOnly);
changed(same); // 입력값이 그대로면(메모만) 서버 안 부름
},
});
}
/** 그 줄 알약을 칸에 이음 — 이으면 손 수량은 지움(칸을 따름) */
async function pickRow(i: number): Promise<boolean> {
const row = cur.담은로직[i];
const logic = logics.get(row.로직);
if (!(await pickCell(lineOf(row.로직)?.알약, logic?.이름 ?? row.로직))) return false;
row.수량 = null;
changed(true);
return true;
}
/** 수량산출 칸 누름 — 손 입력 · 엑셀에서 고르기 */
/** 수량산출 칸 누름 — 로직 줄 */
function openQty(i: number): void {
const row = cur.담은로직[i];
const line = lineOf(row.로직);
openQtyModal(
openQtyFor(
`${row.로직} ${logics.get(row.로직)?.이름 ?? ""}`.trim(),
{
qty: row.수량,
linked: linkedText(line?.알약, line?.분모 ?? ""),
unit: line?.분모 ?? "",
pick: () => pickRow(i),
},
!edit,
opt.goArea,
(qty) => {
row.수량 = qty;
changed(true);
},
row,
line,
line?.분모 ?? "",
);
}
/** 자재 줄 누름 — 수량산출 모달 */
function openMat(i: number): void {
const mat = cur.자재[i];
const line = matOf(mat.코드);
openQtyModal(
`${mat.코드} ${mat.이름 ?? ""}`.trim(),
{
qty: mat.수량,
linked: linkedText(line?.알약, mat.단위 ?? ""),
unit: mat.단위 ?? "",
pick: async () => {
if (!(await pickCell(line?.알약, mat.이름 ?? mat.코드))) return false;
mat.수량 = null;
changed(true);
return true;
},
},
!edit,
opt.goArea,
(qty) => {
mat.수량 = qty;
changed(true);
},
);
openQtyFor(`${mat.코드} ${mat.이름 ?? ""}`.trim(), mat, matOf(mat.코드), mat.단위 ?? "");
}
const addLogic = createButton({