diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 6f435b42d..a5a234504 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -123,3 +123,16 @@ gap: var(--spacing-8); margin-top: var(--spacing-12); } + +/* 보기 갈래(면적산출 오른쪽 패널 · 일위대가 카드) */ +.m02-labview { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-width: 0; +} + +.m02-labview__title { + margin: 0; + font-size: var(--text-body-sm); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 50f8907af..447e53967 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -12,6 +12,7 @@ import { type LogicSummary, type SubBrief, } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; +import type { LaborCalc } from "./M02_MasterTemplete_Labor_Calc"; import { getActiveLayer, getActiveOwner, @@ -215,39 +216,64 @@ export interface LaborDoc { 자재: LaborMat[]; 계산값?: unknown; } -export interface LaborCost { - 노무비: number; - 재료비: number; - 경비: number; - 계: number; +/** 받은 문서를 화면 모양으로 — 빈 칸 · 옛 모양도 받음(빈 일위대가) */ +export function normalizeLabor(doc: unknown, column: string): LaborDoc { + const d = (doc && typeof doc === "object" ? doc : {}) as Partial; + return { + ...d, + 종류: "일위대가", + 열: d.열 ?? column, + 단위: d.단위 ?? null, + 담은로직: (d.담은로직 ?? []).map((r) => ({ + 로직: r.로직, + 메모: r.메모 ?? null, + 입력값: r.입력값 ?? {}, + 수량: r.수량 ?? null, + })), + 자재: (d.자재 ?? []).map((m) => ({ + 코드: m.코드, + 이름: m.이름 ?? null, + 규격: m.규격 ?? null, + 단위: m.단위 ?? null, + 수량: m.수량 ?? null, + })), + }; } -/** calc 줄 — 기준 = 결과단위 그대로 · 반영 = 기준 × 수량 ÷ 배(수량 없거나 돈 아닌 로직은 null) */ -export interface LaborCalcLine { - 로직: string; - 분모: string | null; - 배: number | null; - 수량: number | null; - 수량출처: string | null; - 기준: LaborCost | null; - 반영: LaborCost | null; - 결과?: number | null; - 까닭: string | null; -} -export interface LaborCalcMat { - 코드: string; - 기준단가: number | null; - 수량: number | null; - 수량출처: string | null; - 반영: number | null; - 까닭: string | null; -} -export interface LaborCalc { - 줄: LaborCalcLine[]; - 자재: LaborCalcMat[]; - 기준합: Partial; - 반영합: Partial; + +/** 면적산출 문서의 알약 → {칸, 값}(식 칸 = 서버 `계산값` · 그 밖 = 칸 값 · 수만) — 서버 `area_values` 와 같은 규칙 */ +export function areaLinks(area: unknown): Map { + const out = new Map(); + const doc = (area && typeof area === "object" ? area : {}) as { + 시트?: { + id?: string; + 칸?: Record; + 연결?: { 키?: string; 칸?: string }[]; + }[]; + 계산값?: Record>; + }; + for (const sheet of doc.시트 ?? []) { + const solved = doc.계산값?.[sheet.id ?? ""] ?? {}; + for (const link of sheet.연결 ?? []) { + if (!link.키 || !link.칸) continue; + const raw = solved[link.칸]?.수 ?? sheet.칸?.[link.칸]?.값; + const n = + raw === null || raw === undefined || raw === "" + ? NaN + : Number(String(raw).replace(/,/g, "")); + out.set(link.키, { 칸: link.칸, 값: Number.isFinite(n) ? n : null }); + } + } + return out; } +/** calc 답 모양 · 수량 · 반영 짝 셈 = `M02_MasterTemplete_Labor_Calc.ts`(sub7 · ↔ `M02_Labor_Doc.py`) */ +export type { + CalcLine as LaborCalcLine, + CalcMaterial as LaborCalcMat, + LaborCalc, + Money as LaborCost, +} from "./M02_MasterTemplete_Labor_Calc"; + /** 지금 층이면 `owner` 를 붙인 층 길(`/layers/{층}/labor/{열}…`) */ const laborAt = (layer: Layer, column: string, tail: string): string => { const owner = layer === getActiveLayer() ? getActiveOwner() : null; @@ -255,13 +281,13 @@ const laborAt = (layer: Layer, column: string, tail: string): string => { return `/layers/${layer}/labor/${enc(column)}${tail}${q}`; }; -/** 계산(서버 · 저장 안 함) — `doc` = 화면 캐시(없으면 저장본) · `수량` = 칸 값 바꿔 끼우기 */ +/** 계산(서버 · 저장 안 함) — `doc` = 화면 캐시(없으면 저장본) · `수량` = {알약 키: 칸 값} 바꿔 끼우기 */ export const calcLabor = ( layer: Layer, column: string, doc: LaborDoc | null, 수량?: Record, -): Promise => +): Promise => request(laborAt(layer, column, "/calc"), "POST", { ...(doc ? { doc } : {}), ...(수량 ? { 수량 } : {}), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index dfddeb889..33f599bfe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -11,10 +11,13 @@ import { createButton, createInputField, el, showToast } from "@ui/ui_template_e import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side"; -import type { ItemLogic, Layer } from "./M02_MasterTemplete_Api_Fetch"; +import { readTemplate, type ItemLogic, type Layer } from "./M02_MasterTemplete_Api_Fetch"; +import { settle } from "./M02_MasterTemplete_Labor_Calc"; import { + areaLinks, calcLabor, fetchLogicInputs, + normalizeLabor, type LaborCalc, type LaborDoc, type LaborRow, @@ -26,7 +29,7 @@ import { } from "./M02_MasterTemplete_Labor_Diagram"; import { openLaborInput, openQtyModal } from "./M02_MasterTemplete_Labor_Input"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; -import { logicTable, matTable, qtyText } from "./M02_MasterTemplete_Labor_Rows"; +import { logicTable, matTable } 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"; @@ -35,30 +38,6 @@ import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "@ui/ui_template_general_layout.css"; import "./M02_MasterTemplete_Labor.css"; -/** 받은 문서를 화면 모양으로 — 빈 칸 · 옛 모양도 받음(빈 일위대가) */ -export function normalizeLabor(doc: unknown, column: string): LaborDoc { - const d = (doc && typeof doc === "object" ? doc : {}) as Partial; - return { - ...d, - 종류: "일위대가", - 열: d.열 ?? column, - 단위: d.단위 ?? null, - 담은로직: (d.담은로직 ?? []).map((r) => ({ - 로직: r.로직, - 메모: r.메모 ?? null, - 입력값: r.입력값 ?? {}, - 수량: r.수량 ?? null, - })), - 자재: (d.자재 ?? []).map((m) => ({ - 코드: m.코드, - 이름: m.이름 ?? null, - 규격: m.규격 ?? null, - 단위: m.단위 ?? null, - 수량: m.수량 ?? null, - })), - }; -} - export interface LaborDetailOptions { layer: Layer; column: string; @@ -88,6 +67,11 @@ export function mountLaborDetail( let seq = 0; let timer = 0; let alive = true; + /** 그 열 면적산출의 알약 → 칸 · 값(저장본 · 서버 calc 와 같은 것) */ + let links = new Map(); + const cells = (): Map => new Map([...links].map(([k, v]) => [k, v.칸])); + const given = (): Record => + Object.fromEntries([...links].flatMap(([k, v]) => (v.값 === null ? [] : [[k, v.값]]))); const logicHost = el("div"); const matHost = el("div"); @@ -126,14 +110,14 @@ export function mountLaborDetail( const draw = (): void => { logicHost.replaceChildren( logicTable( - { rows: cur.담은로직, logics, calc, fail }, + { rows: cur.담은로직, logics, calc, fail, cells: cells() }, // 읽기만이면 순서 · 빼기 없음(줄 누름 = 보기 모달은 둠) edit ? { open: openRow, move, remove: removeRow } : { open: openRow }, ), ); matHost.replaceChildren( matTable( - { mats: cur.자재, calc, fail }, + { mats: cur.자재, calc, fail, cells: cells() }, edit ? { open: openMat, remove: removeMat } : { open: openMat }, ), ); @@ -160,9 +144,20 @@ export function mountLaborDetail( }, 150); }; - const changed = (): void => { + /** `qtyOnly` = 수량만 바뀜 — 기준은 그대로라 짝 셈(`Labor_Calc.settle`)으로 바로 곱함(서버 안 부름) */ + const changed = (qtyOnly = false): void => { opt.onChanged(JSON.stringify(cur) !== saved); - recalc(); + if (qtyOnly && calc) { + calc = settle(calc, cur, given()); + draw(); + } 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}` : ""}`; }; /** 아직 안 받은 로직 입력 목록만 받음 */ @@ -203,16 +198,18 @@ export function mountLaborDetail( memo: row.메모, seat: { qty: row.수량, - linked: line && line.수량출처 !== "손" ? qtyText(line, line.분모 ?? "") || null : null, + linked: linkedText(line?.알약, line?.분모 ?? ""), unit: line?.분모 ?? "", }, readOnly: !edit, goArea: opt.goArea, done: (next) => { - row.입력값 = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값; + const values = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값; + const qtyOnly = JSON.stringify(values) === JSON.stringify(row.입력값); + row.입력값 = values; row.메모 = next.메모; row.수량 = next.수량; - changed(); + changed(qtyOnly); }, }); } @@ -224,17 +221,14 @@ export function mountLaborDetail( `${mat.코드} ${mat.이름 ?? ""}`.trim(), { qty: mat.수량, - linked: - line && line.수량출처 && line.수량출처 !== "손" && line.수량 !== null - ? `${line.수량출처} = ${line.수량}` - : null, + linked: linkedText(line?.알약, mat.단위 ?? ""), unit: mat.단위 ?? "", }, !edit, opt.goArea, (qty) => { mat.수량 = qty; - changed(); + changed(true); }, ); } @@ -248,7 +242,7 @@ export function mountLaborDetail( if (cur.담은로직.some((r) => r.로직 === x.키)) return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error"); cur.담은로직.push({ 로직: x.키, 메모: null, 입력값: {}, 수량: null }); - void loadLogics().then(changed); + void loadLogics().then(() => changed()); }), }); const addMat = createButton({ @@ -308,7 +302,14 @@ export function mountLaborDetail( }), ); draw(); - void loadLogics().then(() => { + // 로직 입력 목록 · 면적산출 이은 칸(없으면 빈 것 — 새 구조물)을 받은 뒤 첫 계산 + void Promise.all([ + loadLogics(), + readTemplate(opt.layer, "area", opt.column, null).then( + (got) => (links = areaLinks(got.문서)), + () => undefined, + ), + ]).then(() => { if (alive) recalc(); }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts index 9125b3042..fb987d593 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts @@ -105,11 +105,11 @@ export interface LaborDiagramSource { 줄: { 로직: string; 기준: Money | null; - 반영: Money | null; - 결과?: number | null; - 까닭: string | null; + 반영?: Money | null; + 결과?: unknown; + 까닭?: string | null; }[]; - 자재: { 기준단가: number | null; 반영: number | null }[]; + 자재: { 기준단가: number | null; 반영?: number | null }[]; } | null; /** 담은 자재 수 — 0 이면 자재 카드 없음 */ mats: number; @@ -157,7 +157,7 @@ export function buildLaborDiagram(src: LaborDiagramSource): DiagramData { unit: "", inputs: [], out: mats ? { 노무비: 0, 재료비: sum, 경비: 0, 계: sum } : null, - ...(mats ? { basis: mats.every((m) => m.반영 !== null) ? "반영" : "기준" } : {}), + ...(mats ? { basis: mats.every((m) => typeof m.반영 === "number") ? "반영" : "기준" } : {}), material: true, }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts index e5d913bff..13afe0850 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts @@ -137,7 +137,12 @@ export function openLaborInput(opt: LaborInputOptions): void { if (isBad) bad.add(input.이름); else bad.delete(input.이름); if (value === null) delete draft[input.이름]; - else draft[input.이름] = value; + // 고르기 · 범위 없는 칸도 수로 읽히면 수로 둠(M01 넣을 값과 같게) + else + draft[input.이름] = + typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) + ? Number(value) + : value; }); field.control.disabled = opt.readOnly; field.control.setAttribute("data-labor-input", input.이름); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index f8c637ccc..9e3bc3fbe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -33,16 +33,17 @@ const HEAD: Record<(typeof COSTS)[number], Parameters[0]> = { const num = (v: number): string => Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 }); -/** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」 · 없으면 빈 글 */ +/** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」(칸 = 그 알약을 이은 면적산출 칸) · 없으면 빈 글 */ export function qtyText( - line: { 수량: number | null; 수량출처: string | null } | undefined, + line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined, unit: string, + cells: ReadonlyMap = new Map(), ): string { if (!line || line.수량 === null || line.수량 === undefined) return ""; const v = `${num(line.수량)}${unit ? ` ${unit}` : ""}`; - if (!line.수량출처 || line.수량출처 === "손") - return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`; - return `${tc("Labor_Qty")} ${line.수량출처} = ${v}`; + if (line.수량출처 === "손") return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`; + const cell = (line.알약 && cells.get(line.알약)) || tc("In_QtyCell"); + return `${tc("Labor_Qty")} ${cell} = ${v}`; } /** 금액 칸 하나 — 위 기준(작게) · 아래 반영 */ @@ -100,6 +101,8 @@ export interface LogicTableData { /** calc 답 — null = 계산 중 · 못 셈은 `fail` */ calc: LaborCalc | null; fail?: string | null; + /** 알약 키 → 이은 면적산출 칸(「B6」) */ + cells?: ReadonlyMap; } /** 담은 로직 표 — 보기 갈래는 `on` 없이 */ @@ -114,7 +117,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement const line = lineOf(row.로직); const unit = logic?.결과단위 ?? ""; const values = valueLine(logic?.입력 ?? [], row.입력값); - const qty = qtyText(line, line?.분모 ?? ""); + const qty = qtyText(line, line?.분모 ?? "", data.cells); const note = [values.text, qty].filter(Boolean).join(" · "); const why = line?.까닭 ?? (data.fail && i === 0 ? data.fail : null); const small = (label: string, run: () => void): HTMLElement => @@ -210,6 +213,7 @@ export interface MatTableData { mats: LaborMat[]; calc: LaborCalc | null; fail?: string | null; + cells?: ReadonlyMap; } /** 자재목록 표 — 보기 갈래는 `on` 없이 */ @@ -240,7 +244,7 @@ export function matTable(data: MatTableData, on?: Omit): HT el("td", { className: "m02-labrow__num", text: qty, - attrs: { title: line?.수량출처 ?? "" }, + attrs: { title: qtyText(line, mat.단위 ?? "", data.cells) }, }), el("td", { className: "m02-labrow__num", text: show(line?.기준단가) }), el("td", { className: "m02-labrow__num", text: show(line?.반영) }), @@ -271,7 +275,7 @@ export function matTable(data: MatTableData, on?: Omit): HT tc("Labor_Code"), tc("Col_Name"), tc("Labor_Spec"), - tc("Col_Unit"), + tc("Labor_Unit"), tc("Labor_Qty"), tc("Labor_BasePrice"), tc("Labor_AppliedSum"), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts new file mode 100644 index 000000000..35d805140 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts @@ -0,0 +1,152 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_View.ts + * 일위대가 보기 갈래(39-3 · 고치기 없음) — 그 열 일위대가의 담은 로직 표 + 자재목록 표(기준 · 반영). + * 면적산출 상세 오른쪽 패널(sub3) · 구조물 조합 일위대가 카드(sub1)가 씀. 표 모양은 상세와 한 벌(`Labor_Rows`). + * · `show(층, 열, 수량?)` = 문서 · 로직 입력 목록 · calc 를 받아 그림(최신 응답만) + * · `setQty({알약 키: 칸 값})` = 면적 칸 값이 바뀌면 서버 없이 짝 셈(`Labor_Calc.settle`)으로 바로 곱함 + * (그 열 면적 알약 값 전부를 줌 — 빠진 키는 「수량 없음」) + * · `onRow` = 줄 누름(알약 키 · 이름 · 단위 — 칸 지정 모달 등) · 없으면 누름 없음 + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import { readTemplate, type ItemLogic, type Layer } from "./M02_MasterTemplete_Api_Fetch"; +import { + areaLinks, + calcLabor, + fetchLogicInputs, + normalizeLabor, + type LaborCalc, + type LaborDoc, +} from "./M02_MasterTemplete_Labor_Api"; +import { settle } from "./M02_MasterTemplete_Labor_Calc"; +import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; +import "./M02_MasterTemplete_Labor.css"; + +export interface LaborViewRow { + kind: "logic" | "material"; + /** 로직 키 · 자재 코드 */ + key: string; + /** 면적산출 알약 키(로직 「이름|단위」 · 자재 「자재|MT키」) — calc 전이면 null */ + pill: string | null; + name: string; + unit: string; +} + +export interface LaborViewHandle { + root: HTMLElement; + /** 그 열 일위대가를 읽어 그림 — `수량` = 저장 안 한 면적 칸 값(없으면 저장된 면적산출) */ + show: (layer: Layer, column: string | null, 수량?: Record) => Promise; + /** 면적 칸 값이 바뀜(알약 값 전부) — 받은 기준에 바로 곱함(서버 안 부름) */ + setQty: (수량: Record) => void; + /** 지금 문서(없으면 null) */ + doc: () => LaborDoc | null; + destroy: () => void; +} + +export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewHandle { + const root = el("div", { className: "m02-labview" }); + const logics = new Map(); + let doc: LaborDoc | null = null; + let calc: LaborCalc | null = null; + let fail: string | null = null; + let cells = new Map(); + let seq = 0; + + const paint = (): void => { + if (!doc) return; + const d = doc; + const pick = (row: LaborViewRow): (() => void) | undefined => + onRow ? () => onRow(row) : undefined; + const logicOpen = (i: number): void => { + const row = d.담은로직[i]; + const line = calc?.줄.find((l) => l.로직 === row.로직); + pick({ + kind: "logic", + key: row.로직, + pill: line?.알약 ?? null, + name: logics.get(row.로직)?.이름 ?? row.로직, + unit: line?.분모 ?? "", + })?.(); + }; + const matOpen = (i: number): void => { + const mat = d.자재[i]; + const line = calc?.자재.find((m) => m.코드 === mat.코드); + pick({ + kind: "material", + key: mat.코드, + pill: line?.알약 ?? null, + name: mat.이름 ?? mat.코드, + unit: mat.단위 ?? "", + })?.(); + }; + root.replaceChildren( + el("h4", { className: "m02-labview__title", text: tc("Logics_Title") }), + logicTable( + { rows: d.담은로직, logics, calc, fail, cells }, + onRow ? { open: logicOpen } : undefined, + ), + el("h4", { className: "m02-labview__title", text: tc("Labor_Mats") }), + matTable({ mats: d.자재, calc, fail, cells }, onRow ? { open: matOpen } : undefined), + ); + }; + + const show = async ( + layer: Layer, + column: string | null, + 수량?: Record, + ): Promise => { + const mine = ++seq; + doc = null; + calc = null; + fail = null; + if (!column) return void root.replaceChildren(); + root.replaceChildren(el("p", { className: "m02-labrow__muted", text: tc("Loading") })); + try { + const got = await readTemplate(layer, "labor", column, null); + if (mine !== seq) return; + doc = normalizeLabor(got.문서, column); + const want = doc.담은로직.map((r) => r.로직).filter((k) => !logics.has(k)); + const [inputs, area] = await Promise.all([ + fetchLogicInputs(want), + readTemplate(layer, "area", column, null).then( + (a) => a.문서, + () => null, + ), + ]); + if (mine !== seq) return; + for (const l of inputs.로직) logics.set(l.로직, l); + cells = new Map([...areaLinks(area)].map(([k, v]) => [k, v.칸])); + paint(); // 금액 「…」 + calc = await calcLabor(layer, column, null, 수량); + } catch (error) { + if (mine !== seq) return; + if (!doc) { + root.replaceChildren( + el("p", { + className: "m02-labrow__empty", + text: error instanceof Error ? error.message : tc("Load_Failed"), + }), + ); + return; + } + fail = error instanceof Error ? error.message : tc("Labor_Calc_Fail"); + } + if (mine === seq) paint(); + }; + + return { + root, + show, + setQty: (수량) => { + if (!doc || !calc) return; + calc = settle(calc, doc, 수량); + paint(); + }, + doc: () => doc, + destroy: () => { + seq += 1; + root.remove(); + }, + }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 522751dc9..4675ae01c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -68,6 +68,7 @@ const NUMBER_FIELD: Partial> = { structure: "도번", area: "면적번", material: "재료번", + labor: "일위대가번", }; // 표 부품(sub3) · 도면 부품(sub2) — 내보낸 이름으로 찾음 // 표는 진입 파일만 — 폴더 통째(`*.ts`)면 분수 모듈이 따로 늦게 받는 입구가 되어 번들러 도우미를 @@ -451,6 +452,20 @@ export function createMain( editor = { getDoc: () => comboMerge(loaded, shown.getDoc()), destroy: shown.destroy }; return; } + if (at.kind === "labor") { + // 일위대가 상세(39-3) — 구조물 열마다 하나 · 고침은 이 화면 [저장]으로 + const { mountLaborDetail } = await import("./M02_MasterTemplete_Labor_Detail"); + editor = mountLaborDetail(host, doc, { + layer: at.layer, + column: at.name, + readOnly, + onChanged: (d) => (dirty = d && !readOnly), + goArea: async () => { + if (await confirmLeave()) void open({ ...at, kind: "area" }); + }, + }); + return; + } if (at.kind === "area" || at.kind === "material") { // 면적산출 · 재료산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을 // 열 때만 `import()` 로 받음(계약 `spreadsheet_types.ts` 머리)