/* ============================================================================= * B09_Estimation_Calc_Labor.ts * B09 일위대가표 · 일위대가목록표 셈(PLAN 145-2) — 화면 없음 · 저장 안 함. * * ⚠ 짝 = `B09_Estimation_Calc_Labor.py`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 · * 거울 시험 `resources/tester/test_145_2_b09_labor_calc.py`. * 조작 중은 이 파일이 바로 셈 · [저장] 때 서버가 파이썬 짝으로 정본을 다시 셈. * * 노무 줄 = 노무비목록표 · 재료 줄 = 재료비목록표 · 중기 줄 = 중기목록표 · 대가 줄 = 부른 블록 합계 · * 산근 줄 = 145-1 산근 단가 · % 줄 = 밑수(노무 줄 금액 합) × % / 100 * 비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호) * 줄 금액 ROUNDDOWN(단가 × 수량, 1) · 블록 비목 합 ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합 * 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈) * 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps) * ========================================================================== */ import { gap, num, rounddown, SUPPLY, total, type Gap, type Num, type Row, } from "./B09_Estimation_Calc_Base"; const PARTS = ["labor", "material", "expense"] as const; const TAB = "dg"; const WHY: Record = { labor: "노임 단가 없음", material: "자재 적용 단가 없음", machine: "중기 시간 단가 없음", daega: "부른 대가 합계 없음", sangeun: "산근 단가 없음(145-1)", }; type Parts = Record<(typeof PARTS)[number], Num>; const zeros = (): Parts => ({ labor: 0, material: 0, expense: 0 }); const blanks = (): Parts => ({ labor: null, material: null, expense: null }); function linePrices(line: Row, books: Row, sums: Record): Parts { const { kind, key } = line; const out = zeros(); if (kind === "labor") out.labor = num((books.labor[key] || {}).price); else if (kind === "material") { const row: Row = books.material[key] || {}; // 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈 — 두 번 안 셈) out.material = (SUPPLY as readonly string[]).includes(row.supply) ? 0 : num(row.price); } else if (kind === "machine" || kind === "daega" || kind === "sangeun") { const src: Row = ({ machine: books.machine, daega: sums, sangeun: books.sangeun } as Row)[kind]; const row: Row = src[key] || {}; for (const part of PARTS) out[part] = num(row[part]); } else return blanks(); // blank — 못 펼친 줄 return out; } /** % 줄 밑수 — part:비목 그 비목 줄 금액 합 · lines:labor 노무 줄 금액 합 · line:이름 그 줄 */ function pctBase(spec: string, lines: Row[]): Num { const at = spec.indexOf(":"); const kind = spec.slice(0, at); const name = spec.slice(at + 1); if (kind === "part") return total(lines.map((ln) => ln.amounts[name])); if (kind === "line") { const hit = lines.find((ln) => ln.name === name); return hit ? total(PARTS.map((p) => hit.amounts[p])) : null; } return total(lines.filter((ln) => ln.kind === "labor").map((ln) => ln.amounts.labor)); } function block(b: Row, books: Row, sums: Record, gaps: Gap[]): Row { const lines: Row[] = []; for (const line of b.lines) { if (line.kind === "pct") continue; const qty = num(line.qty); const prices = linePrices(line, books, sums); const amounts = blanks(); for (const part of PARTS) { const p = prices[part]; amounts[part] = p === null || qty === null ? null : rounddown(p * qty, 1); } const ref = line.key ? { ref: { key: line.key } } : {}; // 집계표(145-5) 이음 lines.push({ ...line, ...ref, qty, prices, amounts }); if (qty === null || PARTS.some((part) => prices[part] === null)) { const why = line.why || (qty === null ? "수량 없음" : (WHY[line.kind] ?? "단가 없음")); gaps.push(gap(TAB, b.id, `${b.name} · ${line.name || ""}`, why)); } } for (const line of b.lines) { if (line.kind !== "pct") continue; const base = pctBase(line.base || "lines:labor", lines); const pct = num(line.qty); const prices = zeros(); const amounts = zeros(); const part = (line.part || "expense") as (typeof PARTS)[number]; prices[part] = base; amounts[part] = base === null || pct === null ? null : rounddown((base * pct) / 100, 1); lines.push({ ...line, qty: pct, prices, amounts }); if (amounts[part] === null) gaps.push(gap(TAB, b.id, `${b.name} · ${line.name}`, "% 밑수 없음")); } for (const line of lines) { line.price = total(PARTS.map((p) => line.prices[p])); line.amount = total(PARTS.map((p) => line.amounts[p])); } const parts = blanks(); for (const part of PARTS) { const s = total(lines.map((ln) => ln.amounts[part])); parts[part] = s === null ? null : rounddown(s, 0); } return { id: b.id, key: b.id, kind: b.kind, name: b.name, spec: b.spec, ...(b.spec_raw ? { spec_raw: b.spec_raw } : {}), // 157-4 풍선(파이썬 짝) unit: b.unit, cite: b.cite, lines, ...parts, sum: total(PARTS.map((p) => parts[p])), }; } /** 대가 탭 둘 — daega_blocks(일위대가표) · daega(목록표) · daega_owners + 바탕 gaps 이어 붙임 */ export function computeLabor(daega: Row | null | undefined, sheets: Row, sangeun: Row = {}): Row { const blocks: Row[] = (daega || {}).blocks || []; const index = (rows: Row[] | undefined): Row => Object.fromEntries((rows || []).map((r) => [r.key, r])); const books: Row = { labor: index(sheets.labor), material: index(sheets.material), machine: index(sheets.machine), sangeun: sangeun || {}, }; const byId: Record = Object.fromEntries(blocks.map((b) => [b.id, b])); const no: Record = Object.fromEntries(blocks.map((b, i) => [b.id, i + 1])); const done: Record = {}; const gaps: Gap[] = []; const run = (bid: string, trail: string[]): void => { if (bid in done || trail.includes(bid) || !(bid in byId)) return; for (const line of byId[bid].lines) if (line.kind === "daega") run(line.key, [...trail, bid]); done[bid] = block(byId[bid], books, done, gaps); }; for (const b of blocks) run(b.id, []); const outBlocks = blocks.map((b) => { const got: Row = { ...done[b.id], no: no[b.id] }; for (const line of got.lines) if (line.kind === "daega" && line.key in no) line.ref_no = `대가${no[line.key]}`; for (const line of got.lines) { const sgNo = (books.sangeun[line.key] || {}).no; if (line.kind === "sangeun" && sgNo != null) line.ref_no = `산근 ${sgNo}호표`; // 155-3 비고 } return got; }); const listing = outBlocks.map((b) => ({ no: b.no, key: b.id, kind: b.kind, name: b.name, spec: b.spec, ...(b.spec_raw ? { spec_raw: b.spec_raw } : {}), unit: b.unit, sum: b.sum, labor: b.labor, material: b.material, expense: b.expense, })); return { daega_blocks: outBlocks, daega: listing, daega_owners: { ...((daega || {}).owners || {}) }, gaps: [...(sheets.gaps || []), ...gaps], }; } /** 내역서(145-3)가 읽는 대가 단가 — 구조물 id → 호표 · 비목 단가 · 없으면 null */ export function daegaPrice(sheets: Row, owner: string): Row | null { const bid = (sheets.daega_owners || {})[owner]; const row = (sheets.daega || []).find((r: Row) => r.key === bid); if (!row) return null; return { no: `대가${row.no}`, key: bid, labor: row.labor, material: row.material, expense: row.expense, sum: row.sum, }; }