/* M02 일위대가 수량 · 반영 · 합(PLAN 39-1a · 설계_정함 1장) — 면적 칸 값이 바뀌면 화면이 바로 곱함. * * 짝(CLAUDE.md 5장 ①): `M02_Labor_Doc.py` `settle` 과 같은 규칙 — * 거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`. * 기준(결과단위 그대로)은 서버 calc 길이 셈 · 여기는 받은 기준에 * 수량(손이 칸보다 앞섬 · 칸 = 알약 키 값) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 재료비만. */ export const MONEY_KEYS = ["노무비", "재료비", "경비", "계"] as const; export type Money = Record<(typeof MONEY_KEYS)[number], number>; export type QtySource = "손" | "칸" | "없음"; /** 문서의 수량 칸 — null = 면적산출 알약 · {손} = 손 입력 */ export type Qty = { 손: number } | null; export interface CalcLine { 로직: string; 알약?: string | null; 분모?: string | null; 배: number; 기준: Money | null; 수량?: number | null; 수량출처?: QtySource; 반영?: Money | null; 까닭?: string | null; 결과?: unknown; } export interface CalcMaterial { 코드: string; 알약?: string | null; 기준단가: number | null; 수량?: number | null; 수량출처?: QtySource; 반영?: number | null; 까닭?: string | null; } export interface LaborCalc { 줄: CalcLine[]; 자재: CalcMaterial[]; 기준합?: Money; 반영합?: Money; } /** 수량만 보는 문서 조각 — 담은 로직 · 자재의 손 값 */ export interface LaborQtyDoc { 담은로직?: { 로직: string; 수량?: Qty }[]; 자재?: { 코드: string; 수량?: Qty }[]; } function num(raw: unknown): number | null { if (raw === null || raw === undefined || typeof raw === "boolean") return null; const n = Number(String(raw).replace(/,/g, "").trim()); return String(raw).trim() !== "" && Number.isFinite(n) ? n : null; } function zero(): Money { return { 노무비: 0, 재료비: 0, 경비: 0, 계: 0 }; } function pick( hand: Qty | undefined, key: string | null | undefined, given: Record, ): [number | null, QtySource] { const own = hand ? num(hand.손) : null; if (own !== null) return [own, "손"]; if (key && key in given) return [given[key], "칸"]; return [null, "없음"]; } /** 반영 = 기준 × 수량 ÷ 배 — 수량이 없으면 null */ export function reflect(base: Money, qty: number | null, times: number): Money | null { if (qty === null) return null; const out = zero(); for (const k of MONEY_KEYS) out[k] = (base[k] * qty) / times; return out; } /** 기준이 선 계산 답 + 문서 손 값 + 알약 값(`{키: 값}`) → 수량 · 반영 · 합을 새로 채운 답 */ export function settle( result: LaborCalc, doc: LaborQtyDoc, given: Record, ): LaborCalc { const have: Record = {}; for (const [k, raw] of Object.entries(given)) { const v = num(raw); if (v !== null) have[k] = v; } const hands = new Map((doc.담은로직 ?? []).map((one) => [one.로직, one.수량 ?? null])); const mhands = new Map((doc.자재 ?? []).map((one) => [one.코드, one.수량 ?? null])); const base = zero(); const done = zero(); const add = (sum: Money, m: Money | null): void => { if (m) for (const k of MONEY_KEYS) sum[k] += m[k]; }; const 줄 = result.줄.map((line) => { const [qty, source] = pick(hands.get(line.로직), line.알약, have); const own = line.기준 ? reflect(line.기준, qty, num(line.배) || 1) : null; add(base, line.기준); add(done, own); return { ...line, 수량: qty, 수량출처: source, 반영: own }; }); const 자재 = result.자재.map((one) => { const [qty, source] = pick(mhands.get(one.코드), one.알약, have); const price = num(one.기준단가); const cost = price !== null && qty !== null ? price * qty : null; if (price !== null) add(base, { ...zero(), 재료비: price, 계: price }); if (cost !== null) add(done, { ...zero(), 재료비: cost, 계: cost }); return { ...one, 수량: qty, 수량출처: source, 반영: cost }; }); return { 줄, 자재, 기준합: base, 반영합: done }; }