91 lines
5.3 KiB
JavaScript
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;
|