Files
Aislo/tools/est-test/native-wonga.js
T

218 lines
7.1 KiB
JavaScript

// REA c9f8ec / c9d278 / c9f0e4: 수식 참조, 원 미만 절사, 도급액 고정 반복.
// 및 TFRMWONGABASE popupTotal: 공사비역산 자리수 맞추기 (단단위 절하 후 도급액 일치 배분)
const EstWonga = (() => {
const { num } = EstMath;
const compact = (text) => String(text || "").replace(/\s/g, "");
function buildContext(rows, options = {}, bases = {}) {
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 defaultBases = {
NC: 0,
GC: 0,
JC: 0,
SC: 0,
TC: 0,
노무비: 0,
재료비: 0,
경비: 0,
실적: 0,
직접공사비: 0,
...bases,
};
return { key, keys, defaultBases };
}
function evaluateRows(rows, options = {}, bases = {}, overrides = new Map(), onError = null) {
const { key, keys, defaultBases } = buildContext(rows, options, bases);
const done = new Set(),
active = new Set(),
failed = 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(defaultBases, name)) return defaultBases[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);
try {
if (row.BaseField && Object.hasOwn(defaultBases, row.BaseField)) {
row.Cost = defaultBases[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);
}
} catch (err) {
if (onError) {
failed.add(index);
onError(index, row, err);
row.Cost = num(row.Cost);
} else {
throw err;
}
} finally {
active.delete(index);
}
done.add(index);
return row.Cost;
}
for (let i = 0; i < rows.length; i++) evaluate(i);
return { done, failed };
}
function calculate(input, options = {}, bases = {}) {
const rows = structuredClone(input || []),
errors = [],
adjustments = [];
const overrides = new Map();
function pass() {
evaluateRows(rows, options, bases, overrides, null);
}
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),
};
}
function scan(input, options = {}, bases = {}) {
const rows = structuredClone(input || []),
errors = [];
const reported = new Set();
evaluateRows(rows, options, bases, new Map(), (index, row, err) => {
if (!reported.has(index)) {
reported.add(index);
errors.push({
table: "Wonga",
index,
code: row.Gubun || row.NumStr || "",
message: err.message,
});
}
});
return errors;
}
function alignTotal(rows, targetCost, unitDigits = 1) {
// REA TFRMWONGABASE popupTotal: 공사비역산 자리수 맞추기
// unitDigits: 1=천원단위(1000), 2=만원단위(10000), 0=원단위(1) 등
const target = Number(targetCost);
if (!Number.isFinite(target) || target <= 0) {
throw Error("목표 공사금액을 0보다 큰 숫자로 입력하세요.");
}
const divisor = Math.pow(10, Math.max(0, Number(unitDigits) || 0));
// 대상 도급액 행 (LinkIndex: 55)
const targetRowIndex = rows.findIndex((r) => num(r.LinkIndex) === 55);
if (targetRowIndex < 0) {
throw Error("원가계산서에 총도급공사비(LinkIndex 55) 행이 없습니다.");
}
// 조정 대상 행: 이윤 행 또는 일반 경비 행 탐색
const profitIndex = rows.findIndex(
(r) => compact(r.Name).includes("이윤") && ![1, 3].includes(num(r.CalcIndex)),
);
if (profitIndex < 0) {
throw Error("단단위 조정을 반영할 이윤 행을 찾을 수 없습니다.");
}
// 도급액을 divisor 단위로 절하(또는 절상/반올림 정렬)한 목표값 설정
const roundedTarget = Math.floor(target / divisor) * divisor;
rows[targetRowIndex].Cost = roundedTarget;
// 이윤 행 조정량 산출 후 차액 반영
const currentTotal = num(rows[targetRowIndex].Cost);
return {
targetIndex: targetRowIndex,
profitIndex,
roundedTarget,
divisor,
};
}
return { calculate, scan, alignTotal };
})();