- 변경 차수 상한 4차(K_ORD>4 거부) · 4차 = 5번째 칸(MCOST5·XX5NM·QTY5) - 할증 계수 float32 — 70%·115% 1원 적음 - 손료 E0~E3 = 조달청 XML 구분값뿐 · 계수 곱 없음 - jdl_format.py — 13종 자르는 자리 · 단가 목록 8종 열 이름 · 줄 만들기·읽기·검사 - 지식DB 34번 §17 · REA_MCP_보강 근거 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
620 lines
24 KiB
JavaScript
620 lines
24 KiB
JavaScript
/* -*- 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 < 5) b.qtys.push(0); // 계열 1~5(BOQ QTY5·xAMT5 · REA 34)
|
||
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, 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~5 칸에 [재,노,경]
|
||
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, 0];
|
||
qtys[stage - 1] = n0(spec.qty);
|
||
const units = {};
|
||
if (spec.mls) for (let n = Math.max(2, stage); n <= 5; n++) units[n] = spec.mls.map(n0);
|
||
for (let n = stage; n < 5; 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 = []) {
|
||
// 상한 = 계열 5(K_ORD 4 · 4차 변경) — ST_Prt 0x4b3183 K_ORD ≥ 5 이면 「1차 ~ 4차까지」 오류(REA 34)
|
||
if (n < 2 || n > 5) throw new Error("변경 차수는 2~5");
|
||
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, 5].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;
|