feat(est): reproduce native tables and calculations
This commit is contained in:
@@ -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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user