// EST Plus 2 REA 최종보고서 §3~7: 비목별 계산과 연결 순서. const EstCalc = (() => { const kinds = ['재료비', '노무비', '경비', '실적']; const blank = () => Object.fromEntries(kinds.map(k => [k, 0])); const truncate = (n, digits = 0) => Math.trunc((n + Math.sign(n) * 2e-10) * 10 ** digits) / 10 ** digits; function sourcePrice(source) { if (!source) return 0; const values = (source.prices || []).filter(v => Number.isFinite(v) && v > 0); if (source.priceMode === '최저') return values.length ? Math.min(...values) : 0; if (source.priceMode === '최고') return values.length ? Math.max(...values) : 0; if (source.priceMode === '평균') return values.length ? Math.trunc(values.reduce((a,b) => a+b, 0) / values.length) : 0; if (source.priceMode === '선택') return source.prices?.[source.priceIndex || 0] || 0; return source.price || 0; } function machine(m) { const digits = m.digits ?? 1; const rental = truncate(m.purchase * m.rentalCoefficient * 1e-7, digits); const fuel = truncate(m.fuelPrice * m.fuelQuantity, digits); const supplies = truncate(fuel * m.suppliesRate / 100, digits); const operator = truncate(m.laborPrice * m.laborFactor * m.laborHours, digits); return { 재료비: Math.trunc(fuel + supplies), 노무비: Math.trunc(operator), 경비: Math.trunc(rental), 실적: 0 }; } function calculate(data) { const prices = Object.fromEntries(data.sources.map(s => [s.id, s])); const records = Object.fromEntries(data.units.map(u => [u.id, u])); const units = Object.create(null), visiting = new Set(), errors = []; function calculateUnit(u) { if (units[u.id]) return units[u.id]; if (visiting.has(u.id)) { errors.push(`일위대가 순환 참조: ${u.name}`); return {parts: blank(), lines: [], total: 0}; } visiting.add(u.id); const parts = blank(); const lines = u.lines.map(line => { const source = Object.hasOwn(prices, line.sourceId) ? prices[line.sourceId] : null; const unit = Object.hasOwn(records, line.unitId) ? records[line.unitId] : null; let unitParts = blank(); if (line.type === '일위대가') { if (unit) unitParts = calculateUnit(unit).parts; else errors.push(`${u.name}: 연결된 일위대가 없음`); } else if (line.type === '비율') { const base = line.base || '합계'; if (base === '합계') unitParts[line.kind || '경비'] = kinds.reduce((sum,k) => sum + parts[k], 0); else if (base === '비목별') unitParts = { ...parts }; else unitParts[line.kind || '경비'] = parts[base] || 0; } else if (source) { const machinery = (data.machines || []).find(m => m.sourceId === source.id); if (machinery) unitParts = machine(machinery); else unitParts[source.kind] = sourcePrice(source); } else errors.push(`${u.name}: 연결된 기초단가 없음`); const quantity = line.type === '비율' || line.unit === '%' ? line.quantity / 100 : line.quantity; const components = Object.fromEntries(kinds.map(k => [k, truncate(unitParts[k] * quantity, line.digits ?? 0)])); const amount = kinds.reduce((sum,k) => sum + components[k], 0); if (line.calcIndex !== 2 && line.calcIndex !== 3) for (const k of kinds) parts[k] += components[k]; return { source, unit, components, amount }; }); for (const k of kinds) parts[k] = Math.trunc(parts[k]); visiting.delete(u.id); return units[u.id] = { parts, lines, total: kinds.reduce((sum, k) => sum + parts[k], 0) }; } for (const u of data.units) calculateUnit(u); const direct = blank(); const rows = data.estimate.map(row => { const unit = Object.hasOwn(units, row.unitId) ? units[row.unitId] : null; const parts = Object.fromEntries(kinds.map(k => [k, unit ? Math.trunc(unit.parts[k] * row.quantity) : 0])); for (const k of kinds) direct[k] += parts[k]; return { parts, total: kinds.reduce((sum, k) => sum + parts[k], 0) }; }); const directTotal = kinds.reduce((sum, k) => sum + direct[k], 0); let running = directTotal; const references = Object.assign(Object.create(null), direct, { 직접공사비: directTotal }); const costs = data.costs.map(row => { const base = row.base === '누계' ? running : row.base === '직접공사비' ? directTotal : direct[row.base] || 0; references['누계'] = running; let amount; try { amount = row.formula ? Math.trunc(EstFormula.evaluate(row.formula, references)) : Math.trunc(base * row.rate / 100); } catch (error) { errors.push(`${row.name}: ${error.message}`); amount = 0; } running += amount; references[row.name] = amount; return { base, amount, running }; }); return { units, rows, direct, directTotal, costs, total: running, errors }; } function progress(unitPrice, previous, current, approved, useApproved) { const accepted = useApproved ? approved : current; const cumulative = previous + accepted; return { currentAmount: Math.trunc(unitPrice * current), approvedAmount: Math.trunc(unitPrice * approved), cumulative, cumulativeAmount: Math.trunc(unitPrice * cumulative) }; } return { kinds, calculate, progress, sourcePrice, truncate, machine }; })(); if (typeof module !== 'undefined') module.exports = EstCalc;