373 lines
13 KiB
JavaScript
373 lines
13 KiB
JavaScript
// 원본 테이블 이름과 필드를 유지하며 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 materialBase(rows, options = {}, field = "GC") {
|
|
const levels = [];
|
|
let amount = 0;
|
|
for (const row of rows) {
|
|
if (num(row.LinkIndex) === 54) break;
|
|
const level = num(row.Level);
|
|
if (level > 0) {
|
|
while (levels.length && levels.at(-1).level <= level) levels.pop();
|
|
levels.push({
|
|
level,
|
|
direct: String(row.Name || "")
|
|
.trim()
|
|
.endsWith("*"),
|
|
});
|
|
} else if (
|
|
num(row.CalcIndex) < 2 &&
|
|
(options.GCToWonga || levels.some((group) => group.direct))
|
|
)
|
|
amount += num(row[field]);
|
|
}
|
|
return amount;
|
|
}
|
|
function calculate(input, request = {}) {
|
|
const project = clone(input),
|
|
tables = project.tables,
|
|
options = project.options || {},
|
|
errors = [],
|
|
warnings = [];
|
|
for (const key of allTables) tables[key] ||= [];
|
|
function saveTitles(table, calculated) {
|
|
const byCode = new Map(calculated.map((row) => [row.Code, row]));
|
|
const existing = new Set(tables[table].map((row) => row.Code));
|
|
tables[table] = tables[table]
|
|
.map((row) => {
|
|
const result = byCode.get(row.Code);
|
|
if (!result) return row;
|
|
const merged = { ...row, ...result };
|
|
// Main-row identities belong to a different native table/index.
|
|
// Native title records must preserve their own identities and blob/memo payloads.
|
|
for (const key of [
|
|
"PkID",
|
|
"Index",
|
|
"RecNo",
|
|
"Flag",
|
|
"DifineMemo",
|
|
"SpaceMemo",
|
|
"EscData",
|
|
]) {
|
|
if (Object.hasOwn(row, key)) merged[key] = row[key];
|
|
else delete merged[key];
|
|
}
|
|
return merged;
|
|
})
|
|
.concat(calculated.filter((row) => !existing.has(row.Code)));
|
|
}
|
|
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;
|
|
saveTitles("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);
|
|
}
|
|
for (const row of tables[table] || [])
|
|
for (const previous of row.PreviousCodes || [])
|
|
if (!codes.has(previous)) codes.set(previous, 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 refreshHeadRows =
|
|
kind === 103
|
|
? (request.refreshHeadRows || [])
|
|
.filter((index) => index >= group.start && index < group.start + group.rows.length)
|
|
.map((index) => index - group.start)
|
|
: [];
|
|
const result =
|
|
kind === 103
|
|
? EstRows.head(group.rows, options, resolve, refreshHeadRows)
|
|
: EstText.calculate(
|
|
group.rows,
|
|
options,
|
|
options.SourceRefresh || options.ResourceTracing ? resolve : () => null,
|
|
);
|
|
if (refreshHeadRows.length) request.onHeadRefresh?.(result.refreshErrors);
|
|
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);
|
|
}
|
|
}
|
|
saveTitles(
|
|
"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);
|
|
}
|
|
}
|
|
saveTitles(
|
|
"HeadTitleDat",
|
|
[...heads.keys()].map((code) => completed.get(`103:${code}`)).filter(Boolean),
|
|
);
|
|
const estimate = EstRows.estimate(
|
|
tables.EstMainDat,
|
|
options,
|
|
resolve,
|
|
request.refreshEstimateRows,
|
|
);
|
|
request.onEstimateRefresh?.(estimate.refreshErrors);
|
|
tables.EstMainDat = estimate.rows;
|
|
errors.push(...estimate.errors);
|
|
const direct = estimate.direct;
|
|
const boundary = tables.EstMainDat.findIndex((row) => num(row.LinkIndex) === 54);
|
|
const directMaterial = materialBase(tables.EstMainDat, options);
|
|
const governmentHeading = tables.EstMainDat.findIndex(
|
|
(row, i) => i > boundary && num(row.Level) > 0 && normalize(row.Name).includes("관급"),
|
|
);
|
|
let government = null,
|
|
procurementFees = 0;
|
|
if (governmentHeading >= 0) {
|
|
government = num(tables.EstMainDat[governmentHeading].TC);
|
|
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].CalcIndex) < 2 &&
|
|
(num(tables.EstMainDat[i].LinkIndex) === 57 ||
|
|
(num(tables.EstMainDat[i].LinkIndex) === 99 &&
|
|
num(tables.EstMainDat[i].AnnualSourceLink) === 57))
|
|
)
|
|
procurementFees += num(tables.EstMainDat[i].TC);
|
|
}
|
|
if (options.AutoWongaBase !== false)
|
|
for (const row of tables.Wonga) {
|
|
if (
|
|
(row.VSusic && !/^원금액\s*:/.test(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;
|
|
row.TC = government;
|
|
row.NC = procurementFees;
|
|
if (/^원금액\s*:/.test(row.VSusic))
|
|
row.VSusic = row.VSusic.replace(
|
|
/(원금액\s*:)[\d,.]+/,
|
|
`$1${government.toLocaleString("en-US")}`,
|
|
).replace(/(조달수수료\s*:)[\d,.]+/, `$1${procurementFees.toLocaleString("en-US")}`);
|
|
}
|
|
}
|
|
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: [],
|
|
attachments: [],
|
|
};
|
|
}
|
|
return { calculate, price, groups, empty, allTables, sourceTables, normalize, materialBase };
|
|
})();
|