Files

91 lines
5.3 KiB
JavaScript

// 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;