Files
Aislo/tools/stmate-test/view_prj_boq.js
T
eomsangdonandClaude Opus 5.5 a4c0bc14a6 refactor(stmate-test): 원본에 없는 기능 걷음 — 제비율표 JB 대조 보기 · 보고서 CSV · 계약 저장 공사 대조 · 옛 화면 9개(표준카탈로그·레시피·REA 아키텍처 창·가짜 JDL/XML)
- 원본 대조 = 설치본 실행파일 52개 화면 이름·버튼 글·한글 문자열
- 지움: conv_jb.py · excel_builder.py · view_dan_ilwi.js · view_kanjub_prt.js · /api/conv/jb-* · /api/report/csv · /api/stage/projects · /api/catalog · /api/export-xlsx
- 낡은 시험 삭제(지운 기능 전용): conv_change 제비율표 3 · stage_api 공사별 설정 2 · report_sheets CSV 2줄
- 장부 F4-15 · F1-33 · F6-24 설명 고침 · stmate 시험 786 통과

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

190 lines
6.1 KiB
JavaScript

/**
* STMATE w 8.0 — 계층 내역 재집계(evaluateFullBoqRows) · 단가 반영(syncCostnUnitPrice) · 저장 요청 묶음
* 근거: 16번 §4.2, 22번 §4, 28번 §2.2, 34번 §2.1
*/
(function (global) {
"use strict";
const Calc = global.STMATE_CALC;
const num = Calc.num;
function isStructCode(bd) {
return /^(T\d|E\d|C\d|:)$/.test(String(bd || "").trim());
}
/**
* BOQ11~14.DBF 전수 행(T1~T6 제목, E1~E6 소계/계/총계/총합계, C* 제외금액, 말단 공종행)을
* 실시간 단가·수량과 동기화하여 원단위까지 재집계
*/
function evaluateFullBoqRows(ctx) {
const rawRows = (ctx.dbfData && ctx.dbfData.boq_rows) || [];
const compRes = ctx.computed;
const leafByRecno = {};
const leafByCode = {};
(compRes.boqComputed.rows || []).forEach((r) => {
if (r._recno !== undefined) leafByRecno[r._recno] = r;
if (r.code) leafByCode[r.code] = r;
});
// C* 제외구역 판별 (ENDCODE == SORTCODE)
const exclSet = new Set();
rawRows.forEach((r, idx) => {
const bd = String(r.BDCODE1 || "").trim();
if (bd.startsWith("C")) {
const endCode = r.ENDCODE;
const startIdx = rawRows.findIndex((q) => q.SORTCODE === endCode);
if (startIdx >= 0) {
for (let k = Math.min(startIdx, idx); k <= Math.max(startIdx, idx); k++) {
exclSet.add(k);
}
}
}
});
// 1차: 말단 행 단가·금액 동기화
const rows = rawRows.map((r, idx) => {
const bd = String(r.BDCODE1 || "").trim();
const recno = r._recno !== undefined ? r._recno : idx;
const leaf = leafByRecno[recno] || leafByCode[bd];
const isStruct = isStructCode(bd) || !bd;
if (!isStruct && leaf) {
return {
...r,
_idx: idx,
_recno: recno,
_rowClass: exclSet.has(idx) ? "boq-row-excl" : "boq-row-leaf",
_isExcl: exclSet.has(idx),
QTY1: leaf.qty,
MAMT1: leaf.mUnit,
LAMT1: leaf.lUnit,
SAMT1: leaf.sUnit,
TAMT1: leaf.tUnit,
M_AMT1: leaf.mAmt,
L_AMT1: leaf.lAmt,
S_AMT1: leaf.sAmt,
T_AMT1: leaf.tAmt,
};
}
let rowClass = "";
if (bd.startsWith("T")) rowClass = "boq-row-T";
else if (bd.startsWith("E")) rowClass = "boq-row-E";
else if (bd.startsWith("C")) rowClass = "boq-row-C";
return {
...r,
_idx: idx,
_recno: recno,
_rowClass: rowClass,
_isExcl: exclSet.has(idx),
QTY1: num(r.QTY1),
MAMT1: num(r.MAMT1),
LAMT1: num(r.LAMT1),
SAMT1: num(r.SAMT1),
TAMT1: num(r.TAMT1),
M_AMT1: num(r.M_AMT1),
L_AMT1: num(r.L_AMT1),
S_AMT1: num(r.S_AMT1),
T_AMT1: num(r.T_AMT1),
};
});
// 2차: T*(공종제목) 및 E*(소계/계/총계/총합계), C*(제외금액 총계) 구간 합산 동기화
rows.forEach((r, idx) => {
const bd = String(r.BDCODE1 || "").trim();
if (!bd.startsWith("T") && !bd.startsWith("E") && !bd.startsWith("C")) return;
const endCode = r.ENDCODE;
const partnerIdx = rows.findIndex((q, j) => j !== idx && q.SORTCODE === endCode);
if (partnerIdx < 0 && bd !== "E5" && bd !== "T5") return;
const sIdx = partnerIdx >= 0 ? Math.min(idx, partnerIdx) : 0;
const eIdx = partnerIdx >= 0 ? Math.max(idx, partnerIdx) : rows.length - 1;
const isExclSummary = bd.startsWith("C") || exclSet.has(idx);
let sumM = 0,
sumL = 0,
sumS = 0,
sumT = 0;
for (let k = sIdx; k <= eIdx; k++) {
const sub = rows[k];
const sbd = String(sub.BDCODE1 || "").trim();
if (!sbd || isStructCode(sbd)) continue;
if (!isExclSummary && sub._isExcl) continue;
sumM += num(sub.M_AMT1);
sumL += num(sub.L_AMT1);
sumS += num(sub.S_AMT1);
sumT += num(sub.T_AMT1);
}
if (sumT > 0 || partnerIdx >= 0) {
r.M_AMT1 = sumM;
r.L_AMT1 = sumL;
r.S_AMT1 = sumS;
r.T_AMT1 = sumT;
}
});
return rows;
}
function syncCostnUnitPrice(ctx, code, mVal, lVal, sVal) {
const tVal = mVal + lVal + sVal;
if (ctx.dbfData && ctx.dbfData.costn_rows) {
const row = ctx.dbfData.costn_rows.find((r) => String(r.CODE || "").trim() === code);
if (row) {
Object.assign(row, {
MAMT1: String(mVal),
LAMT1: String(lVal),
SAMT1: String(sVal),
TAMT1: String(tVal),
});
// 적용단가 칸(AMT_61)은 기초단가(M·L·S)만 — 일위대가·산근·중기는 원본도 0
if (/^[MLS]/.test(code) || num(row.AMT_61)) row.AMT_61 = String(tVal);
}
}
const directRes = ctx.state.resources.find((r) => r.code === code);
if (directRes) Object.assign(directRes, { adoptMode: "manual", manualPrice: tVal });
[
["M", "재료비", mVal],
["L", "노무비", lVal],
["S", "경비", sVal],
].forEach(([tag, kind, v]) => {
const synId = `${code}_${tag}`;
const syn = ctx.state.resources.find((r) => r.id === synId);
if (syn) syn.manualPrice = v;
else if (v > 0) {
ctx.state.resources.push({
id: synId,
code: synId,
name: `${code} (${kind})`,
spec: "",
unit: "",
kind,
candidates: [v, 0, 0, 0, 0],
adoptMode: "manual",
manualPrice: v,
convertHourly: false,
isUsd: false,
isSynthetic: true,
});
const ilwi = ctx.state.ilwiItems.find((x) => x.code === code);
if (ilwi && !ilwi.lines.some((ln) => ln.refId === synId)) {
ilwi.lines.push({ refId: synId, qty: 1.0, halPercent: 0, devd: false });
}
}
});
}
// 저장 요청 묶음 — save_payload.js
const buildSavePayload = (ctx) =>
(
global.STMATE_SAVE_PAYLOAD ||
(typeof require !== "undefined" ? require("./save_payload.js") : null)
).buildSavePayload(ctx);
global.STMATE_VIEW_PRJ_BOQ = {
evaluateFullBoqRows,
syncCostnUnitPrice,
buildSavePayload,
};
})(window);