Files
Aislo/tools/stmate-test/change_calc.js
T
eomsangdonandClaude Opus 5.5 5847580670 feat(stmate-test): REA 보강 — 실행·계약·변경 차수·잔여 계산 원본 규칙 · 장부 91.1%
- 지식DB REA 분석 32 실행계약 · 34 변경차수 잔여계산 작업자료(정적분석 · 원본 실행·수정 없음)
- 실행: 중기 실행단가 합계·성분 비례·끝전·차액 성분 · 실행단가·수량 할증 · 계약 변환 옵션·적용제외 · 기성 공사 낙찰율 100%
- 변경: 차수 목록·이어 셈 · 보험료 3모드(변경 2차 이후) · 방법 A/B 5항목 · 퇴직공제·건설기계·표준시장단가·노임 환산 갈래·JCD
- 시험 693 통과 · 브라우저 3건 화면 전수 오류 0 · 총공사비 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 09:29:53 +09:00

619 lines
24 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.
/* -*- coding: utf-8 -*- */
/**
* 설계변경 차수 대비 계산 (change_calc.js) — 내역 머리·계 구조 위에 차수별 수량·단가·금액을 얹어 당초/변경/증감을 냄
* 근거: 지식DB STmate/old/20_분석 08 §2(변경설계 데이터 모델 · 삭제 0 유지 · 신규 안정 ID) · 16 §4.5~4.6(차수 칸)
* · 19 §6.5(변경 직접비 당초·변경·증감) · 35 §4(당초액 유지 4항목) · 24 §7(차수 4벌)
* 정답 대조: resources/tester/test_stmate_stage_change.py
* (영덕 변경 STC BOQ11 n칸 저장 금액 · 같은 폴더 STMATE 엑셀 설계변경증감대비표·금액대비표·설계변경내역서)
*/
const StmateChange = (() => {
const STRUCT = /^(T\d|E\d|C\d|:)$/;
const CARRY_DEFAULT = ["KUNBO", "SLVBO", "YUNBO", "TBUGUM"];
const n0 = (v) => Number(v) || 0;
const clone = (o) => JSON.parse(JSON.stringify(o));
// 차수별 계산 내역 행 — 1·2 차수는 calculateAll 기본 결과, 3·4 는 stages 에 있을 때만
function stageRows(computed, n) {
const boq =
(computed.stages && computed.stages[n] && computed.stages[n].boq) ||
(n === 1 ? computed.boqOrig : n === 2 ? computed.boqRev : null);
return boq ? boq.rows || [] : [];
}
function emptySum() {
return { mAmt: 0, lAmt: 0, sAmt: 0, tAmt: 0 };
}
// 내역 머리(T)·계(E/C)·빈 줄·공종 행을 차례대로 — 머리마다 짝이 되는 닫는 행과 하위 공종 합계
function buildTree(boqRows, state, computed, stageList) {
const stagesN = stageList && stageList.length ? stageList : [1, 2];
const items = (state.boqItems || []).slice();
const byRecno = new Map(items.filter((b) => b._recno != null).map((b) => [b._recno, b]));
const calcBy = {};
for (const n of stagesN) calcBy[n] = new Map(stageRows(computed, n).map((r) => [r.id, r]));
const nodes = [];
const pushLeaf = (b, dbf) => {
const st = {};
for (const n of stagesN) {
const r = calcBy[n].get(b.id);
// 수량 0 칸은 단가도 0(STMATE 저장 · 설계변경내역서 표기 — 삭제·신규 공종)
const z = r && n0(r.qty) === 0 ? 0 : 1;
st[n] = r
? {
qty: n0(r.qty),
mUnit: z * n0(r.mUnit),
lUnit: z * n0(r.lUnit),
sUnit: z * n0(r.sUnit),
tUnit: z * n0(r.tUnit),
mAmt: n0(r.M),
lAmt: n0(r.L),
sAmt: n0(r.S),
tAmt: n0(r.T),
}
: Object.assign({ qty: 0, mUnit: 0, lUnit: 0, sUnit: 0, tUnit: 0 }, emptySum());
}
nodes.push({
kind: "leaf",
id: b.id,
recno: b._recno,
code: b.code,
itnum: (dbf && dbf.ITNUM) || b.itnum || "",
name: b.name,
spec: b.spec || "",
unit: b.unit || "",
gov: !!b.rowType && b.rowType !== "normal",
rowType: b.rowType || "normal",
isNew: !!b.isNew,
st,
});
if (String(b.unit || "").endsWith("%")) {
const nd = nodes[nodes.length - 1];
nd.pct = true;
nd.pctQty = Object.fromEntries(
stagesN.map((n) => {
const own = Array.isArray(b.pctQtys) ? b.pctQtys[n - 1] : null;
return [n, own != null ? n0(own) : n0(dbf && dbf[`QTY${n}`])];
}),
);
}
};
// 새 공종 = 소속 머리의 닫는 행 앞에 끼움(없으면 맨 끝 총합계 앞)
const added = items.filter((b) => b._recno == null);
const addedBy = new Map();
for (const b of added) {
const k = b.parentRecno != null ? b.parentRecno : "__END__";
if (!addedBy.has(k)) addedBy.set(k, []);
addedBy.get(k).push(b);
}
const sortMap = new Map(boqRows.map((r) => [String(r.SORTCODE), r]));
const closerOf = new Map();
for (const r of boqRows) {
const bd = String(r.BDCODE1 || "").trim();
if (/^[EC]\d$/.test(bd) && sortMap.has(String(r.ENDCODE)))
closerOf.set(r._recno, sortMap.get(String(r.ENDCODE))._recno);
}
let lastCloser = null;
boqRows.forEach((r) => {
const bd = String(r.BDCODE1 || "").trim();
if (/^[EC]\d$/.test(bd)) lastCloser = r._recno;
});
boqRows.forEach((r) => {
const bd = String(r.BDCODE1 || "").trim();
if (/^[EC]\d$/.test(bd)) {
const head = closerOf.get(r._recno);
const extra = (addedBy.get(head) || []).concat(
r._recno === lastCloser ? addedBy.get("__END__") || [] : [],
);
extra.forEach((b) => pushLeaf(b, null));
}
if (!bd || bd === ":") {
nodes.push({ kind: "blank", recno: r._recno, itnum: r.ITNUM || "", name: r.NAME1 || "" });
} else if (STRUCT.test(bd)) {
const isHead = bd[0] === "T";
nodes.push({
kind: isHead ? "head" : "close",
code: bd,
level: Number(bd.slice(1)),
zone: bd[0] === "C",
recno: r._recno,
sortcode: String(r.SORTCODE),
endcode: String(r.ENDCODE),
itnum: r.ITNUM || "",
name: r.NAME1 || "",
spec: r.SIZE1 || "",
st: {},
});
} else {
const b = byRecno.get(r._recno);
if (b) pushLeaf(b, r);
else
nodes.push({
kind: "leaf",
missing: true,
recno: r._recno,
code: bd,
itnum: r.ITNUM || "",
name: r.NAME1 || "",
spec: r.SIZE1 || "",
unit: r.UNIT1 || "",
st: Object.fromEntries(stagesN.map((n) => [n, Object.assign({ qty: 0 }, emptySum())])),
});
}
});
if (!boqRows.length) added.forEach((b) => pushLeaf(b, null));
applyPct(nodes, stagesN);
sumHeads(nodes, stagesN);
return { nodes, stages: stagesN };
}
// % 단위 공종(조달수수료 등) — 같은 머리 안 앞선 공종 금액 합 × 수량% · 단가 칸 = 밑수(STMATE 저장 방식)
function applyPct(nodes, stagesN) {
let group = [];
for (const nd of nodes) {
if (nd.kind === "head" || nd.kind === "close") group = [];
if (nd.kind !== "leaf") continue;
if (!nd.pct) {
group.push(nd);
continue;
}
for (const n of stagesN) {
const base = { m: 0, l: 0, s: 0 };
for (const g of group) for (const c of ["m", "l", "s"]) base[c] += n0(g.st[n][c + "Amt"]);
const q = n0(nd.pctQty[n]);
const a = {};
for (const c of ["m", "l", "s"]) a[c] = Math.trunc((base[c] * q) / 100 + 1e-9);
nd.st[n] = {
qty: q,
mUnit: base.m,
lUnit: base.l,
sUnit: base.s,
tUnit: base.m + base.l + base.s,
mAmt: a.m,
lAmt: a.l,
sAmt: a.s,
tAmt: a.m + a.l + a.s,
};
}
}
}
// 머리 합계 — 닫는 행이 C(제외금액 구역)이면 관급 공종까지, 아니면 일반 공종만
function sumHeads(nodes, stagesN) {
const idxBySort = new Map();
nodes.forEach((nd, i) => nd.kind === "head" && idxBySort.set(nd.sortcode, i));
const spans = [];
nodes.forEach((nd, j) => {
if (nd.kind !== "close") return;
const i = idxBySort.get(nd.endcode);
if (i == null) return;
nodes[i].closeIdx = j;
nodes[i].zone = nd.zone;
nd.headIdx = i;
spans.push([i, j, nd.zone]);
});
const inZone = new Array(nodes.length).fill(false);
spans.forEach(([i, j, z]) => z && inZone.fill(true, i, j + 1));
nodes.forEach((nd, i) => {
if (nd.kind !== "head") return;
const end = nd.closeIdx != null ? nd.closeIdx : nodes.length;
const s = Object.fromEntries(stagesN.map((n) => [n, emptySum()]));
for (let k = i + 1; k < end; k++) {
const lf = nodes[k];
if (lf.kind !== "leaf" || (lf.gov && !inZone[i])) continue;
for (const n of stagesN)
for (const key of ["mAmt", "lAmt", "sAmt", "tAmt"]) s[n][key] += n0(lf.st[n][key]);
}
nd.st = s;
if (nd.closeIdx != null) nodes[nd.closeIdx].st = clone(s);
});
}
// 설계변경증감대비표 — 닫는 행 빼고 · 머리는 금액만 · 공종은 수량·금액 당초/변경/증감
function diffTable(tree, a = 1, b = 2) {
return tree.nodes
.filter((nd) => nd.kind !== "close")
.map((nd) => {
const sa = (nd.st && nd.st[a]) || emptySum(),
sb = (nd.st && nd.st[b]) || emptySum();
const leaf = nd.kind === "leaf";
const qa = leaf ? n0(sa.qty) : 0,
qb = leaf ? n0(sb.qty) : 0;
return {
kind: nd.kind,
id: nd.id,
itnum: nd.kind === "head" ? nd.itnum : "",
name: nd.name,
spec: nd.spec || "",
unit: leaf ? nd.unit : "",
code: leaf ? nd.code : "",
qa,
qb,
qd: Math.round((qb - qa) * 1e6) / 1e6,
aa: n0(sa.tAmt),
ab: n0(sb.tAmt),
ad: n0(sb.tAmt) - n0(sa.tAmt),
state: !leaf ? "" : qa === 0 && qb !== 0 ? "신규" : qa !== 0 && qb === 0 ? "삭제" : "",
};
});
}
// 설계변경내역서 — 공종마다 당초 줄·변경 줄(수량·단가·금액 4벌)
function changeStatement(tree, a = 1, b = 2) {
const out = [];
for (const nd of tree.nodes) {
if (nd.kind === "close" || nd.kind === "blank") continue;
for (const n of [a, b]) {
const s = (nd.st && nd.st[n]) || emptySum();
out.push({
kind: nd.kind,
stage: n,
first: n === a,
itnum: n === a && nd.kind === "head" ? nd.itnum : "",
name: n === a ? nd.name : "",
spec: n === a ? nd.spec || "" : "",
unit: n === a && nd.kind === "leaf" ? nd.unit : "",
qty: nd.kind === "leaf" ? n0(s.qty) : null,
tUnit: nd.kind === "leaf" ? n0(s.tUnit) : 0,
lUnit: nd.kind === "leaf" ? n0(s.lUnit) : 0,
mUnit: nd.kind === "leaf" ? n0(s.mUnit) : 0,
sUnit: nd.kind === "leaf" ? n0(s.sUnit) : 0,
tAmt: n0(s.tAmt),
lAmt: n0(s.lAmt),
mAmt: n0(s.mAmt),
sAmt: n0(s.sAmt),
});
}
}
return out;
}
// 원가계산서 항목 순서(금액대비표) — 0원 항목은 빼고 차례 번호
function kanjubRows(kjA, kjB) {
const codes = kjA.items.map((it) => it.code);
return codes
.map((c) => {
const ia = kjA.itemMap[c],
ib = kjB.itemMap[c] || { amount: 0 };
return { code: c, label: ia.label, a: n0(ia.amount), b: n0(ib.amount) };
})
.filter((r) => r.a !== 0 || r.b !== 0)
.map((r) => Object.assign(r, { d: r.b - r.a }));
}
// 설계변경금액대비표 — 머리(최상위 아래 2단까지) + 순공사비 · 간접비 · 일반관리비 · 이윤 · 부가세 · 도급
function amountTable(tree, kjA, kjB, a = 1, b = 2, maxDepth = 2) {
const work = workHeads(tree, maxDepth).map((h) => ({
kind: "head",
itnum: h.itnum,
name: h.name,
spec: h.spec,
a: n0(h.st[a] && h.st[a].tAmt),
b: n0(h.st[b] && h.st[b].tAmt),
}));
const line = (label, va, vb, kind = "sum") => ({ kind, name: label, a: va, b: vb });
const ind = kanjubRows(kjA, kjB).map((r, i) =>
line(`${String(i + 1).padStart(2)}. ${r.label}`, r.a, r.b, "item"),
);
const nextNo = ind.length + 1;
const sub1 = (k) => k.dT + k.kanno + k.indirectExpenseSum;
const rows = work.concat([
line("가. 순공사비계", kjA.dT, kjB.dT),
...ind,
line("나. 소계", sub1(kjA), sub1(kjB)),
line(`${nextNo}. 일반관리비`, kjA.ilbanAmt, kjB.ilbanAmt, "item"),
line("다. 소계", sub1(kjA) + kjA.ilbanAmt, sub1(kjB) + kjB.ilbanAmt),
line(`${nextNo + 1}. 이윤`, kjA.iyunFinal, kjB.iyunFinal, "item"),
line("라. 공급가액", kjA.supplyFinal, kjB.supplyFinal),
line(`${nextNo + 2}. 부가가치세`, kjA.vatFinal, kjB.vatFinal, "item"),
line("마. 도급공사비", kjA.contractFinal, kjB.contractFinal),
]);
if (kjA.grandTotal !== kjA.contractFinal || kjB.grandTotal !== kjB.contractFinal) {
rows.push(line("바. 관급자재대", kjA.govFinal, kjB.govFinal));
rows.push(line("총공사비", kjA.grandTotal, kjB.grandTotal));
}
rows.forEach((r) => (r.d = n0(r.b) - n0(r.a)));
return { rows, totalA: kjA.grandTotal, totalB: kjB.grandTotal };
}
// 머리 고르기 — 관급 구역 빼고 최상위에서 maxDepth 단 아래까지
function workHeads(tree, maxDepth = 2) {
const heads = tree.nodes.filter((nd) => nd.kind === "head");
const top = heads.length ? Math.max(...heads.map((h) => h.level)) : 0;
return heads.filter((h) => !h.zone && top - h.level <= maxDepth);
}
// 변경 총괄내역서(0661) — 머리마다 당초 줄·변경 줄(합계·노무비·재료비·경비)
function summaryRows(tree, a = 1, b = 2, maxDepth = 2) {
const out = [];
for (const h of workHeads(tree, maxDepth))
for (const n of [a, b]) {
const s = h.st[n] || emptySum();
out.push({
stage: n,
itnum: n === a ? h.itnum : "",
name: n === a ? h.name : "",
spec: n === a ? h.spec : "",
tAmt: n0(s.tAmt),
lAmt: n0(s.lAmt),
mAmt: n0(s.mAmt),
sAmt: n0(s.sAmt),
});
}
return out;
}
// 공사원가계산서 당초/변경 대비(PM_Won_A_C) — 비목 차례 · 0원 간접비 항목 뺌 · onlyChanged = 변경분만
function costCompare(kA, kB, onlyChanged = false) {
const L = (group, label, f, note = "") => ({ group, label, a: n0(f(kA)), b: n0(f(kB)), note });
const amt = (code) => (k) => (k.itemMap[code] ? k.itemMap[code].amount : 0);
const add = (st) => (k) => (k.adds || []).filter((x) => x.stage === st);
const addRows = (st, group) => {
const names = [
...new Set(
add(st)(kA)
.concat(add(st)(kB))
.map((x) => x.name),
),
];
return names.map((nm) =>
L(group, nm, (k) =>
add(st)(k)
.filter((x) => x.name === nm)
.reduce((acc, x) => acc + n0(x.amount), 0),
),
);
};
const items = kA.items
.filter((it) => it.code !== "KANNO")
.filter((it) => n0(it.amount) !== 0 || n0(amt(it.code)(kB)) !== 0)
.map((it) => L("경비", it.label, amt(it.code), it.formula));
const rows = [
L("재료비", "직접재료비", (k) => k.dM),
L("재료비", "간접재료비", () => 0),
L("재료비", "작업설,부산물등(△)", () => 0),
L("재료비", "소계", (k) => k.dM),
L("노무비", "직접노무비", (k) => k.dL),
L("노무비", "간접노무비", (k) => k.kanno),
L("노무비", "소계", (k) => k.totalLabor),
L("경비", "산출경비", (k) => k.dS),
...items,
...addRows(1, "경비"),
L("경비", "소계", (k) => k.totalExpense),
L("", "일반관리비", (k) => k.ilbanAmt),
L("", "이윤", (k) => k.iyunFinal),
...addRows(2, ""),
L("", "총원가", (k) => k.supplyFinal),
L("", "부가가치세", (k) => k.vatFinal),
...addRows(3, ""),
L("", "도급공사비", (k) => k.contractFinal),
];
if (kA.govFinal || kB.govFinal) rows.push(L("", "관급자재대", (k) => k.govFinal));
rows.push(...addRows(4, ""));
rows.push(L("", "총공사비", (k) => k.grandTotal));
rows.forEach((r) => (r.d = r.b - r.a));
return onlyChanged ? rows.filter((r) => r.d !== 0) : rows;
}
// 변경율 내역서(0645) — 머리별 증감율(%)
function changeRateTable(tree, a = 1, b = 2) {
return diffTable(tree, a, b)
.filter((r) => r.kind === "head")
.map((r) =>
Object.assign(r, { rate: r.aa ? Math.round((r.ad / r.aa) * 100000) / 1000 : null }),
);
}
// 굴착복구(PCLASS 2)·굴착변경(PCLASS 8) 내역 — 칸 = 계열(ST_KanJub 0x46cb03 · 0x46cb77 차수표)
// 2 = 시비(1)·구비(2)·계 · 8 = 당초(시비 1·구비 2) / 변경(시비 3·구비 4) · 양식 머리 「1단:시비, 2단:구비, 3단:계」
// 「상단:당초단가, 하단:변경단가」(ST_P_Excav 0x46928d · 0x46dfc9) — 지식DB REA 34(양식 차례는 조건부)
function excavTable(tree, pclass = 2) {
const sum = (a, b) => ({ qty: n0(a.qty) + n0(b.qty), tAmt: n0(a.tAmt) + n0(b.tAmt) });
const pick = (nd, n) => (nd.st && nd.st[n]) || emptySum();
const chg = Number(pclass) === 8;
return tree.nodes
.filter((nd) => nd.kind === "head" || nd.kind === "leaf")
.map((nd) => {
const leaf = nd.kind === "leaf";
const part = (si, gu) => {
const a = pick(nd, si),
b = pick(nd, gu);
return {
si: { qty: leaf ? n0(a.qty) : null, unit: leaf ? n0(a.tUnit) : 0, tAmt: n0(a.tAmt) },
gu: { qty: leaf ? n0(b.qty) : null, unit: leaf ? n0(b.tUnit) : 0, tAmt: n0(b.tAmt) },
sum: Object.assign(sum(a, b), leaf ? {} : { qty: null }),
};
};
const row = { kind: nd.kind, itnum: nd.itnum, name: nd.name, spec: nd.spec || "" };
row.unit = leaf ? nd.unit : "";
row.orig = part(1, 2);
if (chg) {
row.rev = part(3, 4);
row.d = row.rev.sum.tAmt - row.orig.sum.tAmt;
}
return row;
});
}
// ── 입력(설계변경 작업) ──
const findItem = (state, id) => (state.boqItems || []).find((b) => b.id === id);
const ensureQtys = (b) => {
if (!Array.isArray(b.qtys)) b.qtys = [n0(b.qty), n0(b.changedQty ?? b.qty), 0, 0];
while (b.qtys.length < 4) b.qtys.push(0);
if (!b.units) b.units = {};
return b;
};
function setQty(state, id, stage, qty) {
const b = ensureQtys(findItem(state, id));
if (String(b.unit || "").endsWith("%")) {
if (!Array.isArray(b.pctQtys)) b.pctQtys = [null, null, null, null];
b.pctQtys[stage - 1] = n0(qty);
return b;
}
b.qtys[stage - 1] = n0(qty);
if (stage === 1) b.qty = n0(qty);
if (stage === 2) b.changedQty = n0(qty);
return b;
}
// 변경 단가 — 차수 2~4 칸에 [재,노,경]
function setUnit(state, id, stage, mls) {
if (stage < 2) throw new Error("당초(1차수) 단가는 단가 화면에서 바꿈");
const b = ensureQtys(findItem(state, id));
b.units[stage] = mls.map(n0);
return b;
}
// 삭제 = 그 차수 수량 0(행은 남김 · 코드 대응 보존)
const deleteInStage = (state, id, stage) => setQty(state, id, stage, 0);
// 신규 공종 — 앞 차수 수량 0 · 안정 ID(NEW_n) · 소속 머리 recno
function addItem(state, spec) {
const used = new Set((state.boqItems || []).map((b) => b.id));
let k = 1;
while (used.has(`NEW_${k}`)) k++;
const stage = spec.stage || 2;
const qtys = [0, 0, 0, 0];
qtys[stage - 1] = n0(spec.qty);
const units = {};
if (spec.mls) for (let n = Math.max(2, stage); n <= 4; n++) units[n] = spec.mls.map(n0);
for (let n = stage; n < 4; n++) qtys[n] = qtys[n] || 0;
const item = {
id: `NEW_${k}`,
code: spec.code || "",
itnum: "",
name: spec.name || "신규 공종",
spec: spec.spec || "",
unit: spec.unit || "",
refId: spec.refId || spec.code || "",
rowType: "normal",
qty: qtys[0],
changedQty: qtys[1],
qtys,
units,
isNew: true,
parentRecno: spec.parentRecno ?? null,
};
state.boqItems.push(item);
return item;
}
// 원본 차수 규칙표(kanjub_parts.js NATIVE · REA 34)
const NATIVE = () =>
(
(typeof window !== "undefined" && window.STMATE_KANJUB_PARTS) ||
(typeof require !== "undefined" ? require("./kanjub_parts.js") : {})
).NATIVE || {};
// 차수 시작 — 앞 차수 수량·단가·원가설정을 복사, 당초액 유지 항목은 직전 계열 금액 고정
// kjPrev = 직전 계열(n−1) 원가계산서 — 원본 직전 계열 = 현재 − 1(Main 0x473ade DEC · 0x4645f4 되부름)
// 기본값 = 원본 제비율 다시 잡기(ST_KanJub 0x47a6ec 안전 2 · 0x47b633 보험료 1) — 이미 고른 값은 그대로
function startStage(state, n, kjPrev, carry = CARRY_DEFAULT, boqRows = []) {
if (n < 2 || n > 4) throw new Error("변경 차수는 2~4");
const dbfBy = new Map(boqRows.map((r) => [r._recno, r]));
for (const b of state.boqItems || []) {
ensureQtys(b);
if (String(b.unit || "").endsWith("%")) {
const r = dbfBy.get(b._recno) || {};
if (!Array.isArray(b.pctQtys))
b.pctQtys = [1, 2, 3, 4].map((k) => (r[`QTY${k}`] ? n0(r[`QTY${k}`]) : null));
b.pctQtys[n - 1] = b.pctQtys[n - 2] != null ? b.pctQtys[n - 2] : n0(r[`QTY${n - 1}`]);
}
b.qtys[n - 1] = b.qtys[n - 2];
if (b.units[n - 1]) b.units[n] = b.units[n - 1].slice();
if (n === 2) b.changedQty = b.qtys[1];
}
const st = (state.kanjubStages = state.kanjubStages || {});
const prev = st[n - 1] || state.kanjubConfig || {};
const cfg = clone(prev);
cfg.slot = n;
cfg.fixed = {};
if (kjPrev)
for (const c of carry) if (kjPrev.itemMap[c]) cfg.fixed[c] = kjPrev.itemMap[c].amount;
if (cfg.anjunAuto == null || cfg.anjunAuto === 1) cfg.anjunAuto = 2;
const pclass = state.projectMeta ? state.projectMeta.pclass : 7;
const dft = NATIVE().changeDefaults
? NATIVE().changeDefaults(pclass == null || pclass === "" ? 7 : pclass, n, cfg.keys || {})
: null;
cfg.keys = cfg.keys || {};
if (
dft &&
(cfg.keys.I_BF_CH_BO == null || cfg.keys.I_BF_CH_BO === "" || n0(cfg.keys.I_BF_CH_BO) === 0)
)
cfg.keys.I_BF_CH_BO = dft.I_BF_CH_BO;
st[n] = cfg;
if (!st[1]) st[1] = Object.assign(clone(state.kanjubConfig || {}), { slot: 1 });
return cfg;
}
// 간접비 항목 변경 계산방식 — recalculated · carry_original(직전 계열 금액 고정 = D_)
// proportional(방법 A) · delta_rate(방법 B) = 원본에 없는 옛 갈래(원본 당초액 잇기는 nativeModes 5항목 · I_BF_CH_BO · I_ANJUNAUTO)
// kjPrev = 직전 계열(n−1) 원가계산서
function setRevMode(state, n, code, mode, kjPrev) {
const cfg = state.kanjubStages[n];
cfg.revModes = cfg.revModes || {};
cfg.fixed = cfg.fixed || {};
if (mode === "carry_original" && kjPrev && kjPrev.itemMap[code])
cfg.fixed[code] = kjPrev.itemMap[code].amount;
else delete cfg.fixed[code];
cfg.revModes[code] = mode === "carry_original" ? "recalculated" : mode;
return cfg;
}
// 원본 당초액 잇기 갈래 — 항목별(KUNBO·YUNBO 비율·증감×율 · SLVBO·TBUGUM·ANJUN 비율 · 나머지 없음)
const nativeModes = (code) => (NATIVE().CHG_ITEMS || {})[code] || [];
// 저장용 — 차수 n 칸 BOQ 행 갱신값(QTYn·xAMTn·x_AMTn)
function boqUpdates(tree, n) {
return tree.nodes
.filter((nd) => nd.recno != null && nd.kind !== "blank")
.map((nd) => {
const s = nd.st[n] || emptySum();
const up = { _recno: nd.recno };
if (nd.kind === "leaf") {
up[`QTY${n}`] = s.qty;
Object.assign(up, {
[`TAMT${n}`]: s.tUnit,
[`LAMT${n}`]: s.lUnit,
[`MAMT${n}`]: s.mUnit,
[`SAMT${n}`]: s.sUnit,
});
}
return Object.assign(up, {
[`T_AMT${n}`]: s.tAmt,
[`L_AMT${n}`]: s.lAmt,
[`M_AMT${n}`]: s.mAmt,
[`S_AMT${n}`]: s.sAmt,
});
});
}
return {
CARRY_DEFAULT,
stageRows,
buildTree,
applyPct,
sumHeads,
diffTable,
changeStatement,
kanjubRows,
amountTable,
workHeads,
summaryRows,
costCompare,
changeRateTable,
excavTable,
setQty,
setUnit,
deleteInStage,
addItem,
startStage,
setRevMode,
nativeModes,
boqUpdates,
};
})();
if (typeof window !== "undefined") window.STMATE_CHANGE = StmateChange;
if (typeof module !== "undefined" && module.exports) module.exports = StmateChange;