// REA c9f8ec / c9d278 / c9f0e4: 수식 참조, 원 미만 절사, 도급액 고정 반복. const EstWonga = (() => { const { num } = EstMath; const compact = (text) => String(text || "").replace(/\s/g, ""); function calculate(input, options = {}, bases = {}) { const rows = structuredClone(input), errors = [], adjustments = []; const key = (row) => String(options.VerFormat === false ? row.NumStr || row.Gubun : row.Gubun || row.NumStr || ""); const keys = new Map(rows.map((row, index) => [key(row), index])); keys.delete(""); rows.forEach((row, index) => { if (compact(row.Name)) keys.set(compact(row.Name), index); }); const overrides = new Map(); function pass() { const done = new Set(), active = new Set(); function effective(index, owner) { const cost = evaluate(index); if ( options.GangubExceptJodalRatio && /산업안전|안전보건/.test(compact(rows[owner]?.Name)) && compact(rows[index].Name).includes("관급") ) return cost - num(rows[index].NC); return cost; } function resolve(name, owner) { if (Object.hasOwn(bases, name)) return bases[name]; const index = keys.get(name) ?? keys.get(compact(name)); return index === undefined ? undefined : effective(index, owner); } function range(start, end, owner) { let a = keys.get(start), b = keys.get(end); if (a === undefined || b === undefined) throw Error(`범위 참조 없음: ${start}:${end}`); if (a > b) [a, b] = [b, a]; let sum = 0; for (let i = a; i <= b; i++) if ([0, 1, 4].includes(num(rows[i].CalcIndex))) sum += effective(i, owner); return sum; } function evaluate(index) { const row = rows[index]; if (overrides.has(index)) return (row.Cost = overrides.get(index)); if (done.has(index)) return num(row.Cost); if (active.has(index)) throw Error(`원가 수식 순환 참조: ${key(row)}`); active.add(index); if (row.BaseField && Object.hasOwn(bases, row.BaseField)) row.Cost = bases[row.BaseField]; else if ( row.VSusic && !/^원금액\s*:/.test(row.VSusic) && ![1, 3].includes(num(row.CalcIndex)) ) row.Cost = Math.trunc( EstExpression.evaluate(row.VSusic, (name) => resolve(name, index), { numericReferences: true, range: (a, b) => range(a, b, index), }), ); else row.Cost = num(row.Cost); active.delete(index); done.add(index); return row.Cost; } for (let i = 0; i < rows.length; i++) evaluate(i); } const targetIndex = rows.findIndex((row) => num(row.LinkIndex) === 55); const target = targetIndex >= 0 ? num(rows[targetIndex].Cost) : 0; try { pass(); if (options.TotalFix && targetIndex >= 0) { const profit = rows.findIndex( (row) => compact(row.Name).includes("이윤") && ![1, 3].includes(num(row.CalcIndex)), ); const vat = rows.findIndex((row) => compact(row.Name).includes("부가")); if (profit < 0) throw Error("도급액 고정: 조정할 이윤 행이 없습니다."); for (let iteration = 0; iteration < 100; iteration++) { const delta = num(rows[targetIndex].Cost) - target; adjustments.push({ iteration, profit: num(rows[profit].Cost), total: num(rows[targetIndex].Cost), difference: delta, }); if (delta === 0) break; overrides.set(profit, num(rows[profit].Cost) - delta); pass(); if (iteration === 99 && vat >= 0) { overrides.set(vat, num(rows[vat].Cost) + target - num(rows[targetIndex].Cost)); pass(); } } if (num(rows[targetIndex].Cost) !== target) throw Error("도급액 고정 계산이 수렴하지 않았습니다."); } } catch (error) { errors.push(error.message); } return { rows, errors, adjustments, total: targetIndex >= 0 ? num(rows[targetIndex].Cost) : num(rows.at(-1)?.Cost), }; } return { calculate }; })();