Files
Aislo/tools/est-test/native-resources.js
T
eomsangdonandClaude Opus 5.5 b1c7d3dbc3 feat(est-test): 동원 확인 탭(장비·단산·일위 대조)·내역서 줄별 펼침
- 호표 1 단위 펼친 항목 합과 호표 금액 대조, 10원 넘게 어긋나면 표시
- 당초값·계산값(노무·재료·경비·실적)과 항목 목록
- 동원 계산에 호표 단위 펼치기(roots) 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 19:33:32 +09:00

227 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const EstResources = (() => {
const { num, components } = EstMath;
// roots: [{ kind, code, WorkCode, WorkName }] 를 주면 내역서 대신 그 호표마다 1 단위를 따로 펼친다(동원 확인 탭)
function calculate(input, byWork = false, roots = null) {
const project = EstNative.calculate({
...structuredClone(input),
options: { ...input.options, ResourceTracing: true },
});
const tables = project.tables,
heads = EstNative.groups(tables.HeadMainDat),
costs = EstNative.groups(tables.CostMainDat),
output = new Map(),
errors = [...project.calculation.errors];
let work = {},
workKey = "";
function record(kind, code, quantity, amount = null) {
const source = (tables[EstNative.sourceTables[kind]] || []).find((row) => row.Code === code);
if (!source) {
errors.push(`동원 원천 없음: ${kind}/${code}`);
return;
}
const id = `${workKey}${kind}:${code}`;
const row = output.get(id) || {
...work,
Code: code,
Name: source.Name,
Size: source.Size,
Unite: source.Unite,
kind,
quantity: 0,
amount: 0,
amountForHours: 0,
};
row.quantity += quantity;
row.amount += amount ?? num(source.TC) * quantity;
if (kind === 105 && quantity === 0 && amount !== null) row.amountForHours += amount;
output.set(id, row);
}
function bidComponents(row, source) {
const classified = [1, 2, 3].includes(num(row.SCheck));
const whole = row.ApplyNacChal && num(source?.TC) ? num(row.TU) / num(source.TC) : 1;
return Object.fromEntries(
components.map((key, index) => [
key,
classified
? whole
: row.ApplyNacChal && num(source?.[key])
? num(row[EstMath.units[index]]) / num(source[key])
: 1,
]),
);
}
function weightedRate(source, rates) {
return num(source?.TC)
? components.reduce((amount, key) => amount + num(source[key]) * rates[key], 0) /
num(source.TC)
: 1;
}
function money(row, multiplier, rates) {
const labels = {
NC: [6, "LZZZZZZZZZZ", "노무비"],
GC: [5, "MZZZZZZZZZZ", "재료비"],
JC: [105, "EZZZZZZZZZZ", "경비"],
SC: [21, "SZZZZZZZZZZ", "표준시장단가"],
};
for (const key of components) {
const amount = num(row[key]) * multiplier * rates[key];
if (!amount) continue;
const [kind, Code, Name] = labels[key],
id = `${workKey}${kind}:${Code}`;
const item = output.get(id) || {
...work,
Code,
Name,
Size: "금액 입력분",
Unite: key === "JC" ? "제외금액" : "원",
kind,
quantity: 0,
amount: 0,
amountForHours: 0,
};
item.amount += amount;
if (key !== "JC") item.quantity += amount;
output.set(id, item);
}
}
function expand(kind, code, multiplier, path, componentRates = null) {
const rates = componentRates || Object.fromEntries(components.map((key) => [key, 1]));
const id = `${kind}:${code}`;
if (path.includes(id)) {
errors.push(`동원 순환 참조: ${id}`);
return;
}
if (kind === 104) {
const group = costs.get(code);
if (!group) {
errors.push(`산근 없음: ${code}`);
return;
}
for (const row of group.rows) {
if (!row.ResourceCode || row.ResourceFactor == null) {
const key = { [-1]: "NC", [-2]: "GC", [-3]: "JC", [-4]: "SC" }[num(row.SCheck)];
if (key && !String(row.LineData || "").includes("\x1a") && num(row.LinkIndex) !== 61)
money({ [key]: num(row.TC) }, multiplier, rates);
continue;
}
const source = tables[EstNative.sourceTables[num(row.ResourceKind)]]?.find(
(item) => item.Code === row.ResourceCode,
);
const directKey = row.ResourceComponent;
const rate = directKey ? rates[directKey] : weightedRate(source, rates);
if (!rate) continue;
if ([103, 104].includes(num(row.ResourceKind)))
expand(
num(row.ResourceKind),
row.ResourceCode,
multiplier * num(row.ResourceFactor),
[...path, id],
Object.fromEntries(
components.map((key) => [
key,
!row.ResourceComponent || row.ResourceComponent === key ? rates[key] : 0,
]),
),
);
else if (num(row.ResourceKind) === 105)
record(105, row.ResourceCode, 0, multiplier * num(row.TC) * rate);
else
record(
num(row.ResourceKind),
row.ResourceCode,
multiplier * num(row.ResourceFactor) * rate,
multiplier * num(row.TC) * rate,
);
}
return;
}
if (kind === 103) {
const group = heads.get(code);
if (!group) {
errors.push(`일위 없음: ${code}`);
return;
}
let rate = 1;
for (const row of group.rows.slice(1)) {
if (num(row.LinkIndex) === 52) {
rate *= num(row.GN) / 100;
break;
}
if (num(row.LinkIndex) === 68)
rate *= num(row.GN) / (["%", "%"].includes(row.Unite) ? 100 : 1);
}
for (const row of group.rows.slice(1)) {
if (num(row.CalcIndex) >= 2 || [51, 52, 68].includes(num(row.LinkIndex))) continue;
const source = tables[EstNative.sourceTables[num(row.LinkIndex)]]?.find(
(r) => r.Code === row.Code,
);
const bid = bidComponents(row, source);
const classified = { 1: "GC", 2: "NC", 3: "JC" }[num(row.SCheck)];
const childRates = Object.fromEntries(
components.map((key) => [
key,
bid[key] * (classified ? rates[classified] : rates[key]),
]),
);
const quantity =
(multiplier * num(row.GN) * rate) / (["%", "%"].includes(row.Unite) ? 100 : 1);
if ([103, 104].includes(num(row.LinkIndex)))
expand(num(row.LinkIndex), row.Code, quantity, [...path, id], childRates);
else if (EstNative.sourceTables[num(row.LinkIndex)]) {
const count = quantity * weightedRate(source, childRates);
const amount =
multiplier *
rate *
components.reduce((amount, key) => amount + num(row[key]) * rates[key], 0);
if (count || amount) record(num(row.LinkIndex), row.Code, count, amount);
} else money(row, multiplier * rate, rates);
}
return;
}
const source = tables[EstNative.sourceTables[kind]]?.find((row) => row.Code === code);
record(kind, code, multiplier * weightedRate(source, rates));
}
for (const [index, root] of (roots || []).entries()) {
workKey = `${index}:`;
work = { WorkCode: root.WorkCode, WorkName: root.WorkName };
expand(root.kind, root.code, 1, []);
}
for (const [index, row] of (roots ? [] : tables.EstMainDat).entries()) {
if (num(row.LinkIndex) === 54) break;
if (
num(row.Level) > 0 ||
num(row.CalcIndex) >= 2 ||
[51, 52, 68].includes(num(row.LinkIndex))
)
continue;
if (byWork) {
workKey = `${index}:`;
work = { WorkCode: row.Code || "", WorkName: row.Name || "" };
}
if (!EstNative.sourceTables[num(row.LinkIndex)]) {
money(row, 1, { NC: 1, GC: 1, JC: 1, SC: 1 });
continue;
}
const source = tables[EstNative.sourceTables[num(row.LinkIndex)]]?.find(
(r) => r.Code === row.Code,
);
expand(num(row.LinkIndex), row.Code, num(row.GN), [], bidComponents(row, source));
}
const rows = [...output.values()];
for (const row of rows)
if (row.kind === 105 && row.amountForHours) {
const price = num(tables.MachTitleDat.find((r) => r.Code === row.Code)?.TC);
if (price) {
row.quantity += row.amountForHours / price;
row.Unite = "HR";
} else {
row.Unite = "제외금액";
row.quantity = 0;
}
}
rows.forEach((row) => delete row.amountForHours);
return { rows, errors: [...new Set(errors)] };
}
return { calculate };
})();