Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Combo.ts
T
eomsangdonandClaude Sonnet 5.5 2d9a469fc7 feat(806-1): 구조물 조합 일위대가 보기 = 일위대가 컨테이너 한 부품 · 단가산출 줄 = 그 SK 문서 모달(수식 탭 · 저장 = 그 SK)
- 조합 상세 · 카드 · 서고 모두 Labor_Table 한 부품 — 조합 전용 칸(면적 잇기 · 수량산출 모달 · 규격 열 · 다이어그램)은 새 Labor_Combo
- 단가산출(SK) 참조 줄 누름 = 새 Labor_SkEdit — 그 SK 문서를 단가산출 상세 부품(수식 탭 첫 화면)으로 열고 [저장] = 수식 저장 → SK 문서 저장 → 일위대가 금액 다시 셈 · 위 층 SK = 읽기만 · 두 번 눌러도 모달 하나
- 옛 조합 전용 표 · 단가산출 보기 모달(Labor_SangeunModal) 지움 · 전국 자재 줄은 Labor_Table 이 그림
- 새 시험 test_806_1_labor_view · 소스 지킴 시험 12 개를 새 자리(Labor_Table · Labor_RowModal · Labor_SkEdit)로 고침 — 옛 Detail 자리 문구가 없어져 낡았음

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 16:31:49 +09:00

203 lines
7.4 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Labor_Combo.ts
* 구조물 조합 일위대가의 덧칸(PLAN 806-1) — 일위대가 컨테이너 부품(`Labor_Table`)에 조합만 얹는 것:
* · 면적산출 잇기 = 그 열 면적산출의 알약 → 칸 · 값(`links`) · 수량산출 모달(43-1 · 손 · 칸 따름 · 잇기 · 풀기) ·
* [저장] 앞 면적 문서 저장(`save`) — 잇기 · 풀기는 화면 문서만
* · 다이어그램 상자(열 상세만) — 담은 로직 · 자재 → 흐름도
* 표 · 줄 · 합계 · 단추는 컨테이너와 한 벌 — 여기엔 조합 전용 칸만 둠.
* ========================================================================== */
import { el, showToast } from "@ui/ui_template_elements";
import { ROUTES } from "@config/config_frontend";
import { navigateTo } from "../A00_Common/router";
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side";
import {
readTemplate,
saveTemplate,
type ItemLogic,
type Layer,
} from "./M02_MasterTemplete_Api_Fetch";
import { linkAreaCell } from "./M02_MasterTemplete_AreaCellPick";
import {
areaLinks,
type LaborCalc,
type LaborDoc,
type LaborQty,
} from "./M02_MasterTemplete_Labor_Api";
import {
buildLaborDiagram,
diagramTitle,
renderComboDiagram,
} from "./M02_MasterTemplete_Labor_Diagram";
import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal";
import { qtyShort } from "./M02_MasterTemplete_Labor_Rows";
import { tc } from "./M02_MasterTemplete_Labor_Text";
type Recalc = (typeof import("../A00_Common/spreadsheet/spreadsheet_recalc"))["recalcWorkbook"];
export interface ComboOptions {
layer: Layer;
column: string;
edit: boolean;
/** 셈 · 면적 자리 주입(142-1) — 없으면 층 길 */
readArea?: () => Promise<{ 문서: unknown; 판?: string | null }>;
}
export interface Combo {
/** 칸 이음 · 값(서버 calc 와 같은 것) */
cells: () => Map<string, string>;
given: () => Record<string, number>;
/** 저장 안 한 칸 연결이 있음 — 서버 셈이 그 칸 값을 써야 함 */
dirty: () => boolean;
setPills: (pills: { 키: string; 이름?: string; 단위?: string }[]) => void;
/** 면적산출 + 엔진을 받음(없으면 빈 것 — 새 구조물) */
load: () => Promise<void>;
/** 수량산출 모달 — 줄 · 자재 같음 · 바뀌면 `changed` */
openQty: (
title: string,
target: { 수량: LaborQty },
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
unit: string,
changed: () => void,
) => void;
/** 면적 문서 저장 · 저장했으면 참 */
save: () => Promise<boolean>;
}
export function createCombo(opt: ComboOptions): Combo {
let links = new Map<string, { 칸: string; 값: number | null }>();
// 엔진(recalc)은 면적 문서와 함께 늦게 받음 — 정적 import 금지(계약 `spreadsheet_types.ts` 머리)
let area: { doc: Workbook; 판: string; recalc: Recalc } | null = null;
let areaDirty = false;
let pills = new Map<string, { 이름?: string; 단위?: string }>();
const cells = (): Map<string, string> => new Map([...links].map(([k, v]) => [k, v.칸]));
/** 칸 잇기 · 풀기(`cell` null) — 화면 문서만(저장은 [저장]) · 됐으면 참 */
const linkCell = (pill: string, cell: string | null, replace = false): boolean => {
if (!area) {
showToast(tc("Labor_NoArea"), "error");
return false;
}
const info = pills.get(pill);
const next = linkAreaCell(
area.doc,
pill,
cell,
{
...(info?.이름 ? { 이름: info.이름 } : {}),
...(info?.단위 ? { 단위: info.단위 } : {}),
},
replace, // 그 칸 다른 알약 — 기본 함께 · [바꾸기]면 뺌(물음은 시트 안)
);
if (!next) {
showToast(tc("Labor_BadCell"), "error");
return false;
}
next.계산값 = area.recalc(next); // 식 칸 값도 바로 — 저장 때 서버가 다시 셈
area.doc = next;
links = areaLinks(next);
areaDirty = true;
return true;
};
return {
cells,
given: () =>
Object.fromEntries(
[...links].flatMap(([k, v]) => (v.값 === null ? [] : [[k, v.값] as [string, number]])),
),
dirty: () => areaDirty,
setPills: (list) => {
pills = new Map(list.map((p) => [p.키, p]));
},
load: () =>
Promise.all([
opt.readArea ? opt.readArea() : readTemplate(opt.layer, "area", opt.column, null),
import("../A00_Common/spreadsheet/spreadsheet_recalc"),
]).then(
([got, { recalcWorkbook }]) => {
area = { doc: got.문서 as Workbook, 판: got.판 ?? "", recalc: recalcWorkbook };
links = areaLinks(got.문서);
},
() => undefined,
),
openQty: (title, target, line, unit, changed) => {
const pill = line?.알약 ?? null;
openQtyModal({
title,
now: qtyShort(line, unit, cells()),
linked: !!pill && links.has(pill),
hand: target.수량?.손 ?? null,
unit,
readOnly: !opt.edit,
sheet: { doc: area?.doc ?? null, layer: opt.layer, column: opt.column, pill },
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();
},
});
},
save: async () => {
if (!area || !areaDirty) return false;
const info = await saveTemplate(opt.layer, "area", opt.column, null, area.판, area.doc);
area.판 = info.판;
areaDirty = false;
return true;
},
};
}
/** 다이어그램 상자(열 상세) — `draw` 를 표가 다시 그릴 때마다 부름 */
export function diagramBox(): {
root: HTMLElement;
draw: (
doc: LaborDoc,
logics: ReadonlyMap<string, ItemLogic>,
calc: LaborCalc | null,
focus: (index: number) => void,
) => void;
} {
const host = el("div");
const root = el("details", {
className: "m01lab__box m01-logic__section m02-labor__dg",
attrs: { open: "" },
children: [el("summary", { text: diagramTitle() }), host],
});
let again: () => void = () => undefined;
root.addEventListener("toggle", () => again());
return {
root,
draw: (doc, logics, calc, focus) => {
again = () => {
if (!root.open) return;
const data = buildLaborDiagram({
rows: doc.담은로직.filter((r) => !r.참조), // ponytail: 참조 줄은 다이어그램에 안 그림 — 그릴 때 카드 꼴 더함
logics: [...logics.values()],
calc,
mats: doc.자재.length,
});
renderComboDiagram(host, data, {
focusInput: (key) => {
const i = doc.담은로직.findIndex((r) => r.로직 === key);
if (i >= 0) focus(i);
},
// 로직 카드 → 단가산출 로직 화면(떠날 때 물음은 UI_Main 의 leave guard)
openLogic: (key) => {
setPendingLogicKey(key);
navigateTo(ROUTES.M01_MASTER_DATA);
},
});
};
again();
},
};
}