feat(est): reproduce native tables and calculations

This commit is contained in:
2026-11-05 06:07:05 +09:00
parent 646eab6245
commit 60ccedcd9a
30 changed files with 5249 additions and 22 deletions
+286
View File
@@ -0,0 +1,286 @@
// 원본 테이블 이름과 필드를 유지하며 REA 전체 계산 순서로 실행한다.
const EstNative = (() => {
const { num, components, units, fixed, sum } = EstMath;
const sourceTables = {
5: "StuffcDat",
6: "LaborcDat",
21: "SilJukDat",
101: "EstiMateDat",
103: "HeadTitleDat",
104: "CostTitleDat",
105: "MachTitleDat",
};
const normalize = (text) =>
String(text || "")
.normalize("NFKC")
.toLowerCase()
.replace(/[\s()[\]]/g, "");
const allTables = [
"StuffcDat",
"LaborcDat",
"SilJukDat",
"EstiMateDat",
"MachGijunDat",
"MachMainDat",
"MachTitleDat",
"CostMainDat",
"CostTitleDat",
"HeadMainDat",
"HeadTitleDat",
"EstMainDat",
"Wonga",
"MulMainDat",
"MulTitleDat",
];
const clone = (value) => structuredClone(value);
function price(row, kind) {
const candidates = Array.from({ length: 6 }, (_, i) => num(row[`Cost${i + 1}`])).filter(
(value) => value !== 0,
);
let total;
switch (num(row.ValueIndex)) {
case 0:
total = candidates.length ? Math.min(...candidates) : 0;
break;
case 1:
total = candidates.length
? Math.trunc(candidates.reduce((a, b) => a + b, 0) / candidates.length)
: 0;
break;
case 2:
total = candidates.length ? Math.max(...candidates) : 0;
break;
case 3:
total = num(row[`Cost${num(row.SelectField) || 1}`]);
break;
default:
total = num(row.InputTC ?? row.TC);
row.InputTC = total;
}
const original = components.map((key) => num(row[`Input${key}`] ?? row[key]));
if ([21, 101].includes(kind))
components.forEach((key, i) => (row[`Input${key}`] = original[i]));
const originalTotal = original.reduce((a, b) => a + b, 0);
const rate =
(1 + num(row.Ratio) / 100) * (num(row.NachalRatio) ? num(row.NachalRatio) / 100 : 1);
const precision = EstMath.precision(row);
const adjust = (value) =>
rate === 1 && !num(row.DigetIndex)
? value
: fixed(value * rate, precision.digits, precision.method);
components.forEach(
(key, i) => (row[key] = [21, 101].includes(kind) && originalTotal ? adjust(original[i]) : 0),
);
if (![21, 101].includes(kind) || !originalTotal)
row[kind === 6 ? "NC" : kind === 21 ? "SC" : kind === 101 ? "JC" : "GC"] = adjust(total);
row.TC = sum(row);
return row;
}
function groups(rows) {
const result = new Map();
let current = null;
rows.forEach((row, index) => {
if (num(row.Index) > 0) {
current = { start: index, rows: [], code: row.Code };
result.set(row.Code, current);
}
if (current) current.rows.push(row);
});
return result;
}
function calculate(input) {
const project = clone(input),
tables = project.tables,
options = project.options || {},
errors = [],
warnings = [];
for (const key of allTables) tables[key] ||= [];
for (const kind of [5, 6, 21, 101])
for (const row of tables[sourceTables[kind]]) price(row, kind);
const machine = EstMachine.calculate(tables.MachMainDat, tables.MachGijunDat, options);
tables.MachMainDat = machine.rows;
tables.MachTitleDat = machine.titles;
errors.push(...machine.errors);
const costs = groups(tables.CostMainDat),
heads = groups(tables.HeadMainDat),
completed = new Map(),
active = new Set();
const sourceIndexes = new Map();
for (const [kind, table] of Object.entries(sourceTables)) {
const codes = new Map(),
names = new Map();
for (const row of tables[table] || []) {
if (row.Code && !codes.has(row.Code)) codes.set(row.Code, row);
const key = normalize(row.Name);
if (!names.has(key)) names.set(key, []);
names.get(key).push(row);
}
sourceIndexes.set(num(kind), { codes, names });
}
function find(row, kind) {
const index = sourceIndexes.get(kind);
let source = row.Code ? index?.codes.get(row.Code) : null;
if (!source && row.Name)
source = (index?.names.get(normalize(row.Name)) || []).find(
(r) =>
normalize(r.Name) === normalize(row.Name) &&
(!row.Size || normalize(r.Size) === normalize(row.Size)) &&
(!row.Unite || normalize(r.Unite) === normalize(row.Unite)),
);
return source;
}
function calculateGroup(kind, code) {
const id = `${kind}:${code}`;
if (completed.has(id)) return completed.get(id);
if (active.has(id)) throw Error(`순환 참조: ${id}`);
const group = (kind === 103 ? heads : costs).get(code);
if (!group) throw Error(`하위 자료 없음: ${id}`);
active.add(id);
const result =
kind === 103
? EstRows.head(group.rows, options, resolve)
: EstText.calculate(group.rows, options, options.SourceRefresh ? resolve : () => null);
const title = {
...tables[sourceTables[kind]].find((row) => row.Code === code),
...result.titles[0],
};
completed.set(id, title);
active.delete(id);
errors.push(...result.errors);
const main = kind === 103 ? "HeadMainDat" : "CostMainDat";
tables[main].splice(group.start, group.rows.length, ...result.rows);
return title;
}
function resolve(row) {
const kind = num(row.LinkIndex);
if (!sourceTables[kind]) return null;
if (options.RecalculateOriginal && num(options.OldRecalcMethod) === 0 && !row.NewItem)
return null;
let source = find(row, kind);
if ([103, 104].includes(kind)) {
const code = source?.Code || row.Code;
if (code) source = calculateGroup(kind, code);
}
if (!source) throw Error(`연결 자료 없음 (${kind}): ${row.Code || row.Name || ""}`);
const result = Object.fromEntries(components.map((key) => [key, num(source[key])]));
if (
options.RecalculateOriginal &&
num(options.OldRecalcMethod) === 1 &&
components.every((key, i) => result[key] === num(row[EstMath.units[i]]))
)
return null;
result.TC = sum(result);
Object.assign(result, {
Code: source.Code,
Name: source.Name,
Size: source.Size,
Unite: source.Unite,
SourceKind: kind,
});
if ([1, 2, 3].includes(num(row.SCheck))) {
const key = { 1: "GC", 2: "NC", 3: "JC" }[num(row.SCheck)];
components.forEach((k) => (result[k] = k === key ? result.TC : 0));
}
return result;
}
for (const [code] of costs) {
try {
calculateGroup(104, code);
} catch (error) {
errors.push(error.message);
}
}
tables.CostTitleDat = [...costs.keys()]
.map((code) => completed.get(`104:${code}`))
.filter(Boolean);
for (const [code] of heads) {
try {
calculateGroup(103, code);
} catch (error) {
errors.push(error.message);
}
}
tables.HeadTitleDat = [...heads.keys()]
.map((code) => completed.get(`103:${code}`))
.filter(Boolean);
const estimate = EstRows.estimate(tables.EstMainDat, options, resolve);
tables.EstMainDat = estimate.rows;
errors.push(...estimate.errors);
const direct = estimate.direct;
const boundary = tables.EstMainDat.findIndex((row) => num(row.LinkIndex) === 54);
const leaves = tables.EstMainDat.slice(
0,
boundary < 0 ? tables.EstMainDat.length : boundary,
).filter((row) => !num(row.Level) && num(row.CalcIndex) < 2);
const directMaterial = options.GCToWonga
? num(direct.GC)
: leaves.filter((row) => num(row.LinkIndex) === 5).reduce((n, row) => n + num(row.GC), 0);
const governmentHeading = tables.EstMainDat.findIndex(
(row, i) => i > boundary && num(row.Level) > 0 && normalize(row.Name).includes("관급"),
);
let government = null;
if (governmentHeading >= 0) {
government = 0;
const level = num(tables.EstMainDat[governmentHeading].Level);
for (
let i = governmentHeading + 1;
i < tables.EstMainDat.length && num(tables.EstMainDat[i].Level) < level;
i++
)
if (!num(tables.EstMainDat[i].Level)) government += num(tables.EstMainDat[i].TC);
}
if (options.AutoWongaBase !== false)
for (const row of tables.Wonga) {
if (row.VSusic || [1, 3].includes(num(row.CalcIndex)) || row.BaseField) continue;
const name = normalize(row.Name);
if (name.includes("직접재료")) row.Cost = directMaterial;
else if (name.includes("간접재료")) row.Cost = num(direct.GC) - directMaterial;
else if (name.includes("직접노무")) row.Cost = num(direct.NC);
else if (name.includes("산출경비")) row.Cost = num(direct.JC);
else if (name.includes("관급자재") && government !== null) row.Cost = government;
}
const wonga = EstWonga.calculate(tables.Wonga, options, {
NC: num(direct.NC),
GC: num(direct.GC),
JC: num(direct.JC),
SC: num(direct.SC),
TC: num(direct.TC),
노무비: num(direct.NC),
재료비: num(direct.GC),
경비: num(direct.JC),
실적: num(direct.SC),
직접공사비: num(direct.TC),
});
tables.Wonga = wonga.rows;
errors.push(...wonga.errors);
const wongaNames = new Map(
wonga.rows
.filter((row) => num(row.CalcIndex) < 2 && row.Gubun)
.map((row) => [normalize(row.Name), row]),
);
for (const row of tables.EstMainDat)
if ([55, 58].includes(num(row.LinkIndex)) && wongaNames.has(normalize(row.Name)))
row.TC = num(wongaNames.get(normalize(row.Name)).Cost);
project.calculation = {
errors: [...new Set(errors)],
warnings,
direct,
total: wonga.total,
wongaAdjustments: wonga.adjustments,
};
return project;
}
function empty() {
return {
schema: "est-web-2",
name: "새 내역서",
tables: Object.fromEntries(allTables.map((key) => [key, []])),
options: { SourceRefresh: true, VerFormat: true, TotalFix: false },
quantities: [],
progress: [],
annual: [],
drawings: [],
};
}
return { calculate, price, groups, empty, allTables, sourceTables, normalize };
})();