/* ============================================================================= * 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, type DiagramLogic, type DiagramOut, } from "./M02_MasterTemplete_Labor_Diagram"; import { lineId } from "./M02_MasterTemplete_Labor_Calc"; 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; given: () => Record; /** 저장 안 한 칸 연결이 있음 — 서버 셈이 그 칸 값을 써야 함 */ dirty: () => boolean; setPills: (pills: { 키: string; 이름?: string; 단위?: string }[]) => void; /** 면적산출 + 엔진을 받음(없으면 빈 것 — 새 구조물) */ load: () => Promise; /** 수량산출 모달 — 줄 · 자재 같음 · 바뀌면 `changed` */ openQty: ( title: string, target: { 수량: LaborQty }, line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined, unit: string, changed: () => void, ) => void; /** 면적 문서 저장 · 저장했으면 참 */ save: () => Promise; } export function createCombo(opt: ComboOptions): Combo { let links = new Map(); // 엔진(recalc)은 면적 문서와 함께 늦게 받음 — 정적 import 금지(계약 `spreadsheet_types.ts` 머리) let area: { doc: Workbook; 판: string; recalc: Recalc } | null = null; let areaDirty = false; let pills = new Map(); const cells = (): Map => 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; }, }; } /** 참조 줄 카드(811-1 · 단가산출 = SK 참조 한 행) — 이름 · 값 = 셈 답 · 넣을 값 없음 · 누름 = 그 줄 모달 */ function refCards(doc: LaborDoc, calc: LaborCalc | null): DiagramLogic[] { return doc.담은로직.flatMap((r) => { if (!r.참조) return []; const line = calc?.줄.find((l) => lineId(l) === lineId(r)); const money = line?.반영 ?? line?.기준; const out: DiagramOut = line?.까닭 ? { 멈춤: line.까닭 } : money ? { ...money } : null; return [ { key: r.참조.키, // 같은 참조 두 번 금지 = 줄 하나 name: r.별칭 || line?.이름 || r.참조.키, unit: line?.분모 ?? "", inputs: [], out, ...(money ? { basis: line?.반영 ? "반영" : "기준" } : {}), }, ]; }); } /** 다이어그램 상자(열 상세) — `draw` 를 표가 다시 그릴 때마다 부름 */ export function diagramBox(): { root: HTMLElement; draw: ( doc: LaborDoc, logics: ReadonlyMap, 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.참조), logics: [...logics.values()], calc, mats: doc.자재.length, }); const at = data.logics.findIndex((l) => l.material); data.logics.splice(at < 0 ? data.logics.length : at, 0, ...refCards(doc, calc)); renderComboDiagram(host, data, { focusInput: (key) => { const i = doc.담은로직.findIndex((r) => r.로직 === key); if (i >= 0) focus(i); }, // 로직 카드 → 단가산출 로직 화면(떠날 때 물음은 UI_Main 의 leave guard) · 참조 카드(811-1) = 그 줄 모달 openLogic: (key) => { const ref = doc.담은로직.findIndex((r) => r.참조?.키 === key); if (ref >= 0) return focus(ref); setPendingLogicKey(key); navigateTo(ROUTES.M01_MASTER_DATA); }, }); }; again(); }, }; }