Files
Aislo/tools/stmate-test/report_boq.js
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면
- 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면
- 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림
- 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표
- 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0)
- DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인)
- 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과

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

460 lines
20 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 -*- */
/**
* 내역 보고서 (report_boq.js) — 설계내역서(0603) · 총괄내역서(0601) · 공사원가계산서(0608)
* 근거: 지식DB STmate/old/20_분석 20 §2.2(간접비 코드 순서 · 0원 항목 빠짐) · §2.6(DISP_EX 제외금액)
* 16 §4(BOQ 계층 T·E·C) · 06 §3(내역 절사) · 25 §2(간접비 식 설명)
* 정답 대조: resources/tester/test_stmate_report_boq.py (원본 STMATE 출력 엑셀)
* 금액 = 계산 엔진(calc.js calculateAll) 차수별 결과 · 구조(공종 계층·순서) = BOQ11.DBF
*/
const StmateReportBoq = (() => {
const C =
(typeof globalThis !== "undefined" && globalThis.STMATE_REPORT_CORE) ||
require("./report_core.js");
const { num, str, trunc, fix, commas } = C;
const PAIR = [
["F", "G"],
["H", "I"],
["J", "K"],
["L", "M"],
];
const STRUCT = /^(T\d|E\d|C\d|:)$/;
const COMP = { 1: "M", 2: "L", 3: "S", 4: "T" };
const rateText = (r) => String(Number(num(r).toFixed(5)));
/**
* 공종 나무 — SORTCODE 순 · T 행이 ENDCODE 의 E/C 행까지 묶음 · C 로 닫히면 제외금액 구역
* 묶음 금액 = 직속 품목 + 제외 아닌 하위 묶음 (원본 엑셀 SUMIF 와 같음)
* 비율 행(PR · 단위 %) = 같은 묶음의 표시(PRPT) 품목 금액 × 율 / 100 원 미만 절사
*/
function boqTree(dbf, stageBoq, slot) {
const raw = ((dbf && dbf.boq_rows) || [])
.slice()
.sort((a, b) => num(a.SORTCODE) - num(b.SORTCODE));
const calcBy = {};
for (const r of (stageBoq && stageBoq.rows) || []) calcBy[r._recno] = r;
const root = { kind: "T", children: [], sum: { M: 0, L: 0, S: 0 }, end: null };
const stack = [root];
const out = [];
let finalRow = null;
for (const r of raw) {
const b = str(r.BDCODE1).trim();
const top = stack[stack.length - 1];
if (/^T\d$/.test(b)) {
const node = { kind: "T", row: r, level: Number(b[1]), children: [], parent: top };
Object.assign(node, { sum: { M: 0, L: 0, S: 0 }, end: num(r.ENDCODE), excluded: false });
top.children.push(node);
stack.push(node);
out.push(node);
continue;
}
if (/^[EC]\d$/.test(b)) {
const at = stack.findIndex((n) => n.end === num(r.SORTCODE));
if (at > 0) {
const node = stack[at];
if (b[0] === "C") node.excluded = true;
stack.length = at;
if (at === 1) finalRow = { kind: "final", node: root };
}
continue;
}
if (!b || b === ":") {
out.push({ kind: b === ":" ? "page" : "blank", row: r });
continue;
}
const item = { kind: "item", row: r, parent: top, code: b };
const unit = str(r.UNIT1).trim();
if (b === "PR" || /%$/.test(unit)) {
item.pct = true;
const inK = COMP[num(r.PRIN)] || "M",
outK = COMP[num(r.PROUT)] || (b[0] === "L" ? "L" : b[0] === "S" ? "S" : "M");
const sibs = top.children.filter((c) => c.kind === "item" && !c.pct);
const marked = sibs.filter((c) => str(c.row.PRPT).trim() === "1");
const base = (marked.length ? marked : sibs).reduce(
(s, c) => s + (inK === "T" ? c.amt.M + c.amt.L + c.amt.S : c.amt[inK]),
0,
);
const q = num(r[`QTY${slot}`]) || num(r.QTY1);
item.qty = q;
item.unitP = { M: 0, L: 0, S: 0 };
item.amt = { M: 0, L: 0, S: 0 };
const k = outK === "T" ? "M" : outK;
item.unitP[k] = base;
item.amt[k] = trunc((base * q) / 100);
} else {
const c = calcBy[r._recno];
item.qty = c ? c.qty : num(r[`QTY${slot}`]);
item.unitP = c ? { M: c.mUnit, L: c.lUnit, S: c.sUnit } : { M: 0, L: 0, S: 0 };
item.amt = c ? { M: c.mAmt, L: c.lAmt, S: c.sAmt } : { M: 0, L: 0, S: 0 };
}
top.children.push(item);
out.push(item);
}
// 묶음 금액 — 아래에서 위로
const sumOf = (node) => {
const s = { M: 0, L: 0, S: 0 };
for (const ch of node.children) {
const v = ch.kind === "T" ? sumOf(ch) : ch.amt;
if (ch.kind === "T" && ch.excluded) continue;
for (const k of "MLS") s[k] += v[k];
}
node.sum = s;
return s;
};
sumOf(root);
return { out, root, finalRow };
}
// 품목 비고 — 코드 모드(SH_TYP1=0) 코드 · 호표 모드 = 대가·산근·중기는 「산근 n호표」 · 자원은 가격지 위치(BQCODE)
// (SH_TYP3=1 이면 자원도 「자재 n」) · 호표 번호 = 종류별 코드순 자리
function itemRemark(dbf, idx, b) {
const ini = (dbf && dbf.start_ini) || {};
if (b === "PR") return "";
if (String(ini.SH_TYP1 ?? "0") === "0") return b;
const r = idx.by[b];
if (!r) return "";
const kind = String(ini[`COD_FMT_${"LMSBXD".indexOf(b[0]) + 1}`] || "");
const no = idx.ofPrefix(b[0]).findIndex((x) => C.code(x) === b) + 1;
if ("BDX".includes(b[0])) return `${kind}${String(no).padStart(4)}호표`;
if (String(ini.SH_TYP3 ?? "2") === "1") return `${kind}${String(no).padStart(5)}`;
return str(r.BQCODE).trim();
}
// 설계내역서(0603) — 계(E)·총계(C) 줄은 묶음 머리 금액으로 올라가고 끝에 총합계 금액 줄
function designSheet(dbf, stage, opts = {}) {
const ini = (dbf && dbf.start_ini) || {};
const order = C.bimokOrder(ini);
const idx = C.costnIndex(dbf);
const name = opts.name || "설계내역서";
const sh = C.newSheet(name, "설 계 내 역 서", C.projectName(dbf), "O");
const h3 = { A: "공 종", B: "명 칭", C: "규 격", D: "수 량", E: "단위" };
Object.assign(h3, { N: "비 고", _k: "head" });
const h4 = { _k: "head" };
order.forEach((k, i) => {
h3[PAIR[i][0]] = C.bimokLabel(ini, k);
h4[PAIR[i][0]] = "단 가";
h4[PAIR[i][1]] = "금 액";
});
C.push(sh, h3);
C.push(sh, h4);
const tree = boqTree(dbf, stage.boq, stage.slot || 1);
const dispEx = `(${str(ini.DISP_EX || "제외금액").trim()})`;
const fill = (cells, unitP, amt, zeroUnit) => {
const u = { M: unitP.M, L: unitP.L, S: unitP.S, T: unitP.M + unitP.L + unitP.S };
const a = { M: amt.M, L: amt.L, S: amt.S, T: amt.M + amt.L + amt.S };
order.forEach((k, i) => {
cells[PAIR[i][0]] = zeroUnit ? 0 : fix(u[k]);
cells[PAIR[i][1]] = fix(a[k]);
});
};
// 쪽 넘김(:) — 빈 줄 하나에 쪽 나눔 표시(인쇄 미리보기에서 다음 쪽으로)
for (const n of tree.out) {
const r = n.row;
if (n.kind === "page") {
C.push(sh, { _k: "page" });
continue;
}
if (n.kind === "blank") {
C.push(sh, { F: 0, _k: "blank" });
continue;
}
const cells = { A: str(r.ITNUM).trim(), B: str(r.NAME1), C: str(r.SIZE1) };
if (n.kind === "T") {
Object.assign(cells, { _k: `T${n.level}` });
fill(cells, { M: 0, L: 0, S: 0 }, n.sum, true);
if (n.excluded) cells.N = dispEx;
} else {
Object.assign(cells, { D: fix(n.qty), E: C.unitText(r.UNIT1), _k: "item", _code: n.code });
fill(cells, n.unitP, n.amt, false);
const rm = itemRemark(dbf, idx, n.code);
if (rm) cells.N = rm;
}
C.push(sh, cells);
}
if (tree.finalRow) {
const cells = { _k: "final" };
const s = tree.finalRow.node.sum;
fill(cells, { M: 0, L: 0, S: 0 }, s, true);
for (const [u] of PAIR) delete cells[u];
C.push(sh, cells);
}
sh._tree = tree;
return sh;
}
// 간접비 항목 — 지식DB 20 §2.2 순서(KAN_ORD_1) · 이름 · 식 설명(25 §2)
const ITEMS = [
["KANNO", "간접노무비", "간 접 노 무 비", "직접노무비"],
["SANBO", "산재보험료", "산 재 보 험 료", "(직접노무비+간접노무비)"],
["KOBO", "고용보험료", "고 용 보 험 료", "(직접노무비+간접노무비)"],
["KUNBO", "건강보험료", "건 강 보 험 료", "직접노무비"],
["SLVBO", "노인장기요양보험료", "노인장기요양보험료", "건강보험료"],
["YUNBO", "연금보험료", "연 금 보 험 료", "직접노무비"],
["TBUGUM", "퇴직공제부금비", "퇴직공제부금비", "직접노무비"],
["ANJUN", "산업안전보건관리비", "산업안전보건관리비", ""],
["KITA", "기타경비", "기 타 경 비", "(직접노무비+간접노무비+재료비)"],
["HWAN", "환경보전비", "환 경 보 전 비", "(재료비 + 직접노무비 + 산출경비)"],
[
"EQIP",
"건설기계대여금지급보증 금액",
"건설기계대여금지급보증 금액",
"(재료비 + 직접노무비 + 산출경비)",
],
];
// 안전관리비 설명 — A) 관급재/1.1 포함 · B) 관급 빼고 1.2배 상한(지식DB 06)
function anjunNote(k) {
const it = k.itemMap.ANJUN;
if (!it) return "";
if (it.modeUsed === "fixed") return "";
const r = it.rate,
add = num(k._anjunAdd);
const noGov = k.dM + k.dL;
const a = trunc((k.anjunBaseRaw * r) / 100) + add;
const b = trunc((trunc((noGov * r) / 100) + add) * 1.2);
return (
`A)(직노+직재+간재+관급재/1.1)x${rateText(r)}% + ${commas(add)} = ${commas(a)}\n` +
`B)<(직노+직재+간재)x${rateText(r)}% + ${commas(add)}>의 1.2배 = ${commas(b)}`
);
}
/**
* 원가 줄 — 순공사비 → 간접비(0원 빠짐) → 소계 → 일반관리비 → 이윤 → 단계별 추가금액 → 부가세 → 도급 → 관급 → 총공사비
* fixedKeys = D_ 표시(저장액 고정) 항목 — 식 설명 없음
*/
function costLines(k, cfg) {
const fixedKeys = new Set(Object.keys((cfg && cfg.fixed) || {}));
const govMat = k.govRawTotal + k.govFeeTotal;
const exp = [];
for (const [code, short, long, base] of ITEMS) {
const it = k.itemMap[code];
if (!it || !it.amount) continue;
let note = "";
if (!fixedKeys.has(code))
note = code === "ANJUN" ? anjunNote(k) : `${base} x ${rateText(it.rate)}%`;
exp.push({ code, short, long, amount: it.amount, rate: it.rate, note });
}
const adds = (st) => (k.adds || []).filter((a) => a.stage === st && num(a.amount));
return { exp, adds, govMat, fixedKeys };
}
// 총괄내역서(0601) — 공종 묶음 줄 + 원가 계산 줄
function summarySheet(dbf, stage, design, opts = {}) {
const ini = (dbf && dbf.start_ini) || {};
const order = C.bimokOrder(ini);
const cols = ["D", "E", "F", "G"];
const name = opts.name || "총괄내역서";
const sh = C.newSheet(name, "총 괄 내 역 서", C.projectName(dbf), "H");
const h3 = { A: "공 종", B: "명 칭", C: "규 격", _k: "head" };
order.forEach((k, i) => (h3[cols[i]] = C.bimokLabel(ini, k)));
C.push(sh, h3);
const minLevel = opts.minLevel ?? 2;
const put4 = (cells, s) => {
const v = { M: s.M, L: s.L, S: s.S, T: s.M + s.L + s.S };
order.forEach((k, i) => (cells[cols[i]] = v[k]));
};
for (const n of design._tree.out)
if (n.kind === "T" && n.level >= minLevel) {
const cells = { A: str(n.row.ITNUM).trim(), B: str(n.row.NAME1), C: str(n.row.SIZE1) };
put4(Object.assign(cells, { _k: `T${n.level}` }), n.sum);
C.push(sh, cells);
}
const k = stage.kanjub;
const L = costLines(k, stage.cfg);
const direct = design._tree.finalRow
? design._tree.finalRow.node.sum
: { M: k.dM, L: k.dL, S: k.dS };
const ga = { A: " 가.", B: "순공사비계", _k: "sub" };
put4(ga, direct);
C.push(sh, ga);
let no = 0;
const line = (label, amount, note, extra) => {
no += 1;
const cells = { B: `${String(no).padStart(2)}. ${label}`, D: amount, _k: "cost" };
if (note) cells.E = note;
return C.push(sh, Object.assign(cells, extra || {}));
};
const sub = (a, label, amount) =>
C.push(sh, { A: ` ${a}`, B: ` ${label}`, D: amount, _k: "sub" });
line(
"간접노무비",
k.kanno,
L.fixedKeys.has("KANNO") ? "" : `직접노무비 x ${rateText(k.itemMap.KANNO.rate)}%`,
);
for (const e of L.exp.filter((x) => x.code !== "KANNO"))
line(
e.short,
e.amount,
e.note,
e.code === "ANJUN" && L.govMat > 0 && !L.fixedKeys.has("ANJUN")
? { C: "A)관급재/1.1포함 적용" }
: null,
);
for (const a of L.adds(1)) line(a.name, num(a.amount));
const na = k.pureConstructionCost;
sub("나.", "소 계", na);
line(
"일반관리비",
k.ilbanAmt,
L.fixedKeys.has("ILBAN") ? "" : `(나.소 계) x ${rateText(num(stage.cfg?.rates?.ILBAN))}%`,
);
sub("다.", "소 계", na + k.ilbanAmt);
const iyunR = rateText(num(stage.cfg?.rates?.IYUN));
line("이 윤", k.iyunFinal, `(다.소계 - 재료비) x ${iyunR}% = ${commas(k.iyunRaw)}`);
for (const a of L.adds(2)) line(a.name, num(a.amount));
sub("라.", "공급가액", k.supplyFinal);
const vn = vatNote(stage.cfg);
line("부가가치세", k.vatFinal, vn === "총 원 가 x 10%" ? "공급가액 x 10%" : vn);
for (const a of L.adds(3)) line(a.name, num(a.amount));
sub("마.", "도급공사비", k.contractFinal);
if (k.govFinal)
line(
govLabel(dbf, stage, false),
k.govFinal,
`원자재대: ${commas(L.govMat)} (조달수수료: ${commas(k.govFeeTotal)} 포함)`,
);
for (const a of L.adds(4)) line(a.name, num(a.amount));
sub("바.", "총공사비", k.grandTotal);
return sh;
}
// 부가세 설명 — I_BUGA 갈래(공급가액 · 산림조합 · 면세품 · 재료비)
// 설정 두 꼴 — 옛 vatMode/vatExempt · 새 keys(I_BUGA · C_BUGA_M 원키)
const VAT_MODES = { 0: "supply_10", 1: "mat_10", 2: "zero", 3: "forest", 4: "forest_exempt" };
function vatNote(cfg) {
const keys = (cfg && cfg.keys) || {};
const m =
(cfg && cfg.vatMode) || ("I_BUGA" in keys ? VAT_MODES[num(keys.I_BUGA)] : "") || "supply_10";
const exempt = cfg && cfg.vatExempt != null ? num(cfg.vatExempt) : num(keys.C_BUGA_M);
if (m === "forest_exempt") return `(재료비 - 면세품목:₩${commas(exempt)} + 산출경비) x 10%`;
if (m === "forest") return "(재료비 + 산출경비) x 10%";
if (m === "mat_10") return "재료비 x 10%";
return m === "zero" ? "" : "총 원 가 x 10%";
}
// 관급 이름 — 25 판부터 도급자 설치 관급(C_KWANGUP_D)은 「관급자재대(도급자설치)」(울진 2025 출력)
function govLabel(dbf, stage, wide) {
const ini = (dbf && dbf.start_ini) || {};
const kv = (dbf && dbf.prjlst_smt) || {};
const d = num(kv[`${stage.slot}_C_KWANGUP_D`]);
if (num(ini.PRG_V) >= 25 && d > 0) return "관급자재대(도급자설치)";
return wide ? "관 급 자 재 대" : "관급자재대";
}
// 원가계산서 줄 — 차수 하나의 { key, 칸, 금액, 설명 }
function wonRows(dbf, stage) {
const k = stage.kanjub;
const L = costLines(k, stage.cfg);
const rows = [];
const add = (key, cells, amount, note) => rows.push({ key, cells, amount, note: note || "" });
add("dM", { A: "순 공 사 원 가", B: "재 료 비", C: "직 접 재 료 비" }, k.dM);
add("iM", { C: "간 접 재 료 비" }, 0);
add("by", { C: "작업설,부산물등(△)" }, 0);
add("sM", { C: "소 계" }, k.dM);
add("dL", { B: "노 무 비", C: "직 접 노 무 비" }, k.dL);
const kn = L.fixedKeys.has("KANNO") ? "" : `직접노무비 x ${rateText(k.itemMap.KANNO.rate)}%`;
add("KANNO", { C: "간 접 노 무 비" }, k.kanno, kn);
add("sL", { C: "소 계" }, k.totalLabor);
add("dS", { B: "경 비", C: "산 출 경 비" }, k.dS);
for (const e of L.exp.filter((x) => x.code !== "KANNO")) {
const note =
e.code === "ANJUN" && stage.cfg.anjunAuto === 2 && stage.slot > 1
? "당초금액 + 안전관리비 증감액"
: e.note;
add(e.code, { C: e.long }, e.amount, note);
}
for (const a of L.adds(1)) add(`A1${a.name}`, { C: a.name }, num(a.amount));
add("sS", { C: "소 계" }, k.totalExpense);
const ilR = rateText(num(stage.cfg?.rates?.ILBAN));
add(
"ILBAN",
{ A: "일 반 관 리 비" },
k.ilbanAmt,
L.fixedKeys.has("ILBAN") ? "" : `순공사원가 x ${ilR}%`,
);
const iyR = rateText(num(stage.cfg?.rates?.IYUN));
add("IYUN", { A: "이 윤" }, k.iyunFinal, `(노무비+경비+일반관리비) x ${iyR}%`);
for (const a of L.adds(2)) add(`A2${a.name}`, { A: a.name }, num(a.amount));
add("SUP", { A: "총 원 가" }, k.supplyFinal);
add("VAT", { A: "부 가 가 치 세" }, k.vatFinal, vatNote(stage.cfg));
for (const a of L.adds(3)) add(`A3${a.name}`, { A: a.name }, num(a.amount));
add("CON", { A: "도 급 공 사 비" }, k.contractFinal);
if (k.govFinal) {
const gn = `원자재대: ${commas(L.govMat)} (조달수수료: ${commas(k.govFeeTotal)} 포함)`;
add("GOV", { A: govLabel(dbf, stage, true) }, k.govFinal, gn);
}
for (const a of L.adds(4)) add(`A4${a.name}`, { A: a.name }, num(a.amount));
add("GRAND", { A: "총 공 사 비" }, k.grandTotal);
return rows;
}
/**
* 공사원가계산서(0608) — 비목 · 금액 · 구성비(총공사비 대비 %) · 비고(식 설명)
* base(당초 차수)를 주면 변경 양식 — 당초 · 변경 · 증감 · 구성비(변경) · 비고(변경)
*/
function wonSheet(dbf, stage, base) {
const sh = C.newSheet(
"공사원가계산서",
"공 사 원 가 계 산 서",
C.projectName(dbf),
base ? "I" : "G",
);
const now = wonRows(dbf, stage);
const grand = stage.kanjub.grandTotal || 1;
if (!base) {
C.put(sh, 3, { A: "구 분 ", D: "금 액", E: "구 성 비", F: "비 고", _k: "head" });
C.put(sh, 4, { A: " 비 목", _k: "head" });
now.forEach((r, i) => {
const c = Object.assign({ D: r.amount, E: (r.amount / grand) * 100, _k: "cost" }, r.cells);
if (r.note) c.F = r.note;
C.put(sh, 5 + i, c);
});
} else {
C.put(sh, 3, { A: "구 분 ", D: "금 액", G: "구 성 비", H: "비 고", _k: "head" });
C.put(sh, 4, { A: " 비 목", D: "당 초", E: "변 경", F: "증 감", _k: "head" });
const old = Object.fromEntries(wonRows(dbf, base).map((r) => [r.key, r]));
now.forEach((r, i) => {
const o = old[r.key] ? old[r.key].amount : 0;
const c = Object.assign({ D: o, E: r.amount, F: r.amount - o, _k: "cost" }, r.cells);
c.G = (r.amount / grand) * 100;
if (r.note) c.H = r.note;
C.put(sh, 5 + i, c);
});
}
const last = sh.rows[sh.rows.length - 1];
last[base ? "G" : "E"] = 100;
return sh;
}
/**
* 차수 맥락 — calc.js 결과(stages)에서 n 차수 내역·원가 · 설정(kanjubStages)
* 안전관리비 기초액(C_ANJUN_G)은 식 설명용
*/
function stageOf(state, computed, n = 1) {
const st = (computed && computed.stages && computed.stages[n]) || {
boq: computed.boqOrig,
kanjub: computed.kanjubOrig,
};
const cfg = (state.kanjubStages && state.kanjubStages[n]) || state.kanjubConfig || {};
const kanjub = Object.assign({}, st.kanjub, { _anjunAdd: num(cfg.anjunBaseAdd) });
if (cfg.fixed && cfg.fixed.ANJUN != null && kanjub.itemMap.ANJUN)
kanjub.itemMap = Object.assign({}, kanjub.itemMap, {
ANJUN: Object.assign({}, kanjub.itemMap.ANJUN, { modeUsed: "fixed" }),
});
return { boq: st.boq, kanjub, cfg, slot: n };
}
function buildBoqSheets(dbf, state, computed, n = 1, opts = {}) {
const stage = stageOf(state, computed, n);
const design = C.applyForm(designSheet(dbf, stage, opts), dbf, "0603");
const summary = C.applyForm(summarySheet(dbf, stage, design, opts), dbf, "0601");
const base = n > 1 && opts.changeForm !== false ? stageOf(state, computed, 1) : null;
const won = C.applyForm(wonSheet(dbf, stage, base), dbf, "0608");
return { stage, 공사원가계산서: won, 총괄내역서: summary, 설계내역서: design };
}
return { buildBoqSheets, boqTree, designSheet, summarySheet, wonSheet, stageOf, ITEMS };
})();
if (typeof window !== "undefined") window.STMATE_REPORT_BOQ = StmateReportBoq;
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportBoq;