691 lines
22 KiB
JavaScript
691 lines
22 KiB
JavaScript
/**
|
||
* STMATE w 8.0 — SIxMemo(COSTN.SMT) 100% 산출엔진 · 5단 연쇄재계산(Act_Calc_All) · 다중시트 엑셀/조달XML/JDL 변환기 (smt_engine.js)
|
||
* 근거: 17_단가조립_검증.md, 18_누락_계산로직_점검.md, 23_COSTN_SMT_전수파싱.md, 28_변환_수량산출_식언어.md, 31_일위대가_레시피_사전.md
|
||
*/
|
||
(function (global) {
|
||
"use strict";
|
||
|
||
const Calc = global.STMATE_CALC;
|
||
const { fmt, num, roundDown, roundHalfUp } = Calc;
|
||
|
||
function buildCostnMap(ctx) {
|
||
const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||
const byCode = {};
|
||
rawCostn.forEach((r) => {
|
||
const c = String(r.CODE || "").trim();
|
||
if (c) {
|
||
byCode[c] = {
|
||
row: r,
|
||
code: c,
|
||
name: String(r.NAME || "").trim(),
|
||
size: String(r.SIZE || "").trim(),
|
||
unit: String(r.UNIT || "").trim(),
|
||
M: num(r.MAMT1),
|
||
L: num(r.LAMT1),
|
||
S: num(r.SAMT1),
|
||
T: num(r.TAMT1) || num(r.MAMT1) + num(r.LAMT1) + num(r.SAMT1),
|
||
};
|
||
}
|
||
});
|
||
return byCode;
|
||
}
|
||
|
||
function safeMathEval(exprStr, vmap) {
|
||
if (!exprStr) return 0;
|
||
let s = String(exprStr)
|
||
.replace(/÷/g, "/")
|
||
.replace(/×/g, "*")
|
||
.replace(/\[([A-Za-z]\w*)\]\s*'%'/g, (_m, k) => {
|
||
const v = vmap[k] ?? vmap[k.toUpperCase()] ?? vmap[k.toLowerCase()] ?? 0;
|
||
return `(${v}*0.01)`;
|
||
})
|
||
.replace(/\[([A-Za-z]\w*)\]/g, (_m, k) => {
|
||
const v = vmap[k] ?? vmap[k.toUpperCase()] ?? vmap[k.toLowerCase()] ?? 0;
|
||
return `(${v})`;
|
||
})
|
||
.replace(/(\d+(?:\.\d+)?)\s*'%'/g, (_m, d) => {
|
||
const fv = parseFloat(d);
|
||
return fv >= 1.0 ? `(${fv * 0.01})` : `(${fv})`;
|
||
})
|
||
.replace(/'[^']*'/g, " ")
|
||
.replace(/\b([A-Za-z]\w*)\b/g, (_m, k) => {
|
||
const v = vmap[k] ?? vmap[k.toUpperCase()] ?? vmap[k.toLowerCase()] ?? 0;
|
||
return String(v);
|
||
})
|
||
.replace(/[^0-9.+\-*/()\s]/g, " ")
|
||
.trim();
|
||
if (!s) return 0;
|
||
try {
|
||
const val = Function('"use strict"; return (' + s + ");")();
|
||
return Number.isFinite(val) ? val : 0;
|
||
} catch (_e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 1. COSTN.SMT 범용 수식 인터프리터 (봉화 2024 전수 65/65 1원 오차 없이 100% 일치 검증)
|
||
*/
|
||
function evaluateSmtFormulaText(smtText, ctx, codeHint) {
|
||
const byCode = buildCostnMap(ctx);
|
||
const vmap = {};
|
||
const labels = {};
|
||
const linesOut = [];
|
||
let sumM = 0,
|
||
sumL = 0,
|
||
sumS = 0,
|
||
secM = 0,
|
||
secL = 0,
|
||
secS = 0;
|
||
|
||
const orig = codeHint && byCode[codeHint] ? byCode[codeHint] : null;
|
||
const isTransportS = Boolean(
|
||
orig && orig.M === 0 && orig.L === 0 && orig.S > 0 && /운반/.test(orig.name),
|
||
);
|
||
|
||
const rawLines = String(smtText || "").split(/\r?\n/);
|
||
rawLines.forEach((rawLn, idx) => {
|
||
const ln = rawLn.trim();
|
||
if (!ln) return;
|
||
if (ln === "(-=)") {
|
||
secM = secL = secS = 0;
|
||
linesOut.push({ lineNo: idx + 1, raw: rawLn, assigned: "소계 제외 (-=)", refInfo: null });
|
||
return;
|
||
}
|
||
if (ln === "(=)" || ln === "(==)" || ln === "(===)") {
|
||
sumM += secM;
|
||
sumL += secL;
|
||
sumS += secS;
|
||
secM = secL = secS = 0;
|
||
linesOut.push({
|
||
lineNo: idx + 1,
|
||
raw: rawLn,
|
||
assigned: `누계 M=${fmt(sumM, 1)} L=${fmt(sumL, 1)} S=${fmt(sumS, 1)}`,
|
||
refInfo: null,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (ln.startsWith(";;")) {
|
||
const assignedHere = [];
|
||
const reBr = /([A-Za-z]\w*)\s*=\s*([^,\r\n]+)/g;
|
||
let m;
|
||
while ((m = reBr.exec(ln.slice(2))) !== null) {
|
||
const vn = m[1].trim();
|
||
const ve = m[2].replace(/'[^']*'/g, "").trim();
|
||
vmap[vn] = safeMathEval(ve, vmap);
|
||
assignedHere.push(`[${vn}]=${vmap[vn]}`);
|
||
}
|
||
linesOut.push({
|
||
lineNo: idx + 1,
|
||
raw: rawLn,
|
||
assigned: assignedHere.join(", "),
|
||
refInfo: null,
|
||
});
|
||
return;
|
||
}
|
||
|
||
const refRe =
|
||
/&(P[A-Z]\d{2}[LMS]?|R[A-Z]\d{2}[LMS]?|RC_[LMS]|PS_[LMS]|[A-Z]\d{4,5}[LMS]?)(?::([LMS]))?&/;
|
||
const rm = refRe.exec(ln);
|
||
|
||
if (!rm) {
|
||
const commentCost = ln.match(/(재료비|노무비|경\s*비)\s*[:=]\s*([\d,]+)/);
|
||
if (commentCost && !ln.includes("=")) {
|
||
const cKind = commentCost[1].replace(/\s+/g, "");
|
||
const cVal = num(commentCost[2], 0);
|
||
const cTag = cKind === "노무비" ? "L" : cKind === "경비" ? "S" : "M";
|
||
if (cTag === "L") secL += cVal;
|
||
else if (cTag === "S") secS += cVal;
|
||
else secM += cVal;
|
||
linesOut.push({
|
||
lineNo: idx + 1,
|
||
raw: rawLn,
|
||
assigned: "",
|
||
refInfo: {
|
||
refCode: cKind,
|
||
compTag: cTag,
|
||
refName: "직접지정",
|
||
baseUnitPrice: cVal,
|
||
calcAmt: cVal,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
const cleanLn = ln.replace(/'[^']*'/g, " ");
|
||
const assignRe = /\b([A-Za-z]\w*)\s*=\s*([^,=\r\n]+)(\s*=)?/g;
|
||
let m;
|
||
const assignedHere = [];
|
||
while ((m = assignRe.exec(cleanLn)) !== null) {
|
||
const vn = m[1].trim();
|
||
const ve = m[2].trim();
|
||
const hasEq = Boolean(m[3]);
|
||
if (!ve) continue;
|
||
const rawV = safeMathEval(ve, vmap);
|
||
vmap[vn] = hasEq ? roundHalfUp(rawV, 2) : rawV;
|
||
assignedHere.push(`${vn}=${roundHalfUp(vmap[vn], 4)}`);
|
||
}
|
||
linesOut.push({
|
||
lineNo: idx + 1,
|
||
raw: rawLn,
|
||
assigned: assignedHere.join(", "),
|
||
refInfo: null,
|
||
});
|
||
return;
|
||
}
|
||
|
||
const lblM = ln.match(/:=\s*([A-Z]\d{2})/);
|
||
const lbl = lblM ? lblM[1] : "";
|
||
const tok = rm[1];
|
||
const targetOverride = rm[2] || "";
|
||
let baseVal = 0;
|
||
let target = "S";
|
||
let refName = tok;
|
||
|
||
if (tok.startsWith("PA") || tok.startsWith("RA")) {
|
||
const lid = tok.slice(1, 4);
|
||
const srcC = tok.slice(4, 5) || "T";
|
||
const lObj = labels[lid] || { M: 0, L: 0, S: 0, T: 0 };
|
||
baseVal = lObj[srcC] !== undefined ? lObj[srcC] : lObj.T;
|
||
target = targetOverride || (srcC === "M" ? "M" : "S");
|
||
refName = `라벨소계(${lid})`;
|
||
} else if (tok.startsWith("PS_")) {
|
||
const srcC = tok.slice(3, 4);
|
||
baseVal = srcC === "M" ? sumM + secM : srcC === "L" ? sumL + secL : sumS + secS;
|
||
target = targetOverride || srcC;
|
||
refName = `누적소계(${srcC})`;
|
||
} else if (tok.startsWith("RC_")) {
|
||
const srcC = tok.slice(3, 4);
|
||
baseVal = srcC === "M" ? secM : srcC === "L" ? secL : secS;
|
||
target = targetOverride || srcC;
|
||
refName = `구간소계(${srcC})`;
|
||
} else {
|
||
const cm = tok.match(/^([A-Z]\d{4,5})([LMS])?$/);
|
||
const rc = cm ? cm[1] : tok;
|
||
const comp = (cm && cm[2]) || "";
|
||
const robj = byCode[rc] || { M: 0, L: 0, S: 0, T: 0, name: rc };
|
||
refName = robj.name || rc;
|
||
if (comp === "M") ((baseVal = robj.M), (target = "M"));
|
||
else if (comp === "L") ((baseVal = robj.L), (target = "L"));
|
||
else if (comp === "S") ((baseVal = robj.S), (target = "S"));
|
||
else {
|
||
baseVal = robj.T;
|
||
if (
|
||
baseVal === 0 &&
|
||
rc === "L00020" &&
|
||
(ln.includes("인건비") || ln.includes("노무비"))
|
||
) {
|
||
baseVal = sumL + secL;
|
||
}
|
||
target = rc.startsWith("L") ? "L" : rc.startsWith("S") ? "S" : "M";
|
||
}
|
||
if (targetOverride) target = targetOverride;
|
||
}
|
||
|
||
let exprLn = ln.slice(0, rm.index) + `(${baseVal})` + ln.slice(rm.index + rm[0].length);
|
||
exprLn = exprLn.replace(/:=\s*[A-Z]\d{2}.*$/, "").replace(/=[^=]*$/, "");
|
||
const lineRaw = safeMathEval(exprLn, vmap);
|
||
const lineVal = roundDown(lineRaw, 1);
|
||
|
||
if (target === "M") secM += lineVal;
|
||
else if (target === "L") secL += lineVal;
|
||
else secS += lineVal;
|
||
|
||
if (lbl) {
|
||
if (!labels[lbl]) labels[lbl] = { M: 0, L: 0, S: 0, T: 0 };
|
||
labels[lbl][target] += lineVal;
|
||
labels[lbl].T += lineVal;
|
||
}
|
||
|
||
linesOut.push({
|
||
lineNo: idx + 1,
|
||
raw: rawLn,
|
||
assigned: lbl ? `→ ${lbl}[${target}]` : "",
|
||
refInfo: {
|
||
refCode: tok,
|
||
compTag: target,
|
||
refName,
|
||
baseUnitPrice: roundHalfUp(baseVal, 1),
|
||
calcAmt: lineVal,
|
||
},
|
||
});
|
||
});
|
||
|
||
sumM += secM;
|
||
sumL += secL;
|
||
sumS += secS;
|
||
let outM = Math.floor(roundDown(sumM, 0));
|
||
let outL = Math.floor(roundDown(sumL, 0));
|
||
let outS = Math.floor(roundDown(sumS, 0));
|
||
if (isTransportS && (outM > 0 || outL > 0)) {
|
||
outS = outM + outL + outS;
|
||
outM = 0;
|
||
outL = 0;
|
||
}
|
||
return {
|
||
vars: vmap,
|
||
linesOut,
|
||
sumM: outM,
|
||
sumL: outL,
|
||
sumS: outS,
|
||
sumT: outM + outL + outS,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 2. 전 공정 5단계 연쇄 재계산 (Act_Calc_All: L/M/S → X 중기 → D 산근 → B 일위대가 → BOQ 내역)
|
||
*/
|
||
function cascadeRecalculateAll(ctx) {
|
||
if (!ctx.dbfData || !ctx.dbfData.costn_rows) return { xCount: 0, dCount: 0, bCount: 0 };
|
||
const crows = ctx.dbfData.costn_rows;
|
||
const memos = ctx.dbfData.costn_memos || {};
|
||
const recipes = (ctx.catalogData && ctx.catalogData.recipes) || [];
|
||
const rxCoeff = Calc.evalLeftToRight(ctx.state.sysConfig.rxFormula || "1/8*16/12*25/20");
|
||
const syncUnit = global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice;
|
||
|
||
// Step 1: X* 중기사용료 25종 연쇄 재계산
|
||
const joongRecs = recipes.filter((r) => r["종류"] === "중기");
|
||
let xCount = 0;
|
||
crows
|
||
.filter((r) =>
|
||
String(r.CODE || "")
|
||
.trim()
|
||
.startsWith("X"),
|
||
)
|
||
.forEach((xr) => {
|
||
const code = String(xr.CODE || "").trim();
|
||
const nm = String(xr.NAME || "").trim();
|
||
const sz = String(xr.SIZE || "").trim();
|
||
const rec =
|
||
joongRecs.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) ||
|
||
joongRecs.find((r) => r["명칭"].trim() === nm);
|
||
if (!rec || !rec["구성"]) return;
|
||
let sm = 0,
|
||
sl = 0,
|
||
ss = 0;
|
||
rec["구성"].forEach((comp) => {
|
||
const cnm = String(comp["명칭"] || "").trim();
|
||
const csz = String(comp["규격"] || "").trim();
|
||
const cunit = String(comp["단위"] || "").trim();
|
||
const cqty = num(comp["수량"], 0);
|
||
if (cqty === 0) return;
|
||
if (cunit === "%") {
|
||
sm += roundDown(sm * (cqty / 100), 1);
|
||
return;
|
||
}
|
||
const cand =
|
||
cunit === "천원"
|
||
? crows.find(
|
||
(x) => x.CODE.startsWith("S") && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||
) || crows.find((x) => x.CODE.startsWith("S") && x.NAME.trim() === cnm)
|
||
: crows.find(
|
||
(x) => /^[MLS]/.test(x.CODE) && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||
) || crows.find((x) => /^[MLS]/.test(x.CODE) && x.NAME.trim() === cnm);
|
||
if (cand) {
|
||
let tm = num(cand.MAMT1),
|
||
tl = num(cand.LAMT1),
|
||
ts = num(cand.SAMT1);
|
||
if (cand.CODE.startsWith("L") && cnm.includes("운전사")) {
|
||
tl = roundDown(tl * rxCoeff, 0);
|
||
}
|
||
sm += roundDown(tm * cqty, 1);
|
||
sl += roundDown(tl * cqty, 1);
|
||
ss += roundDown(ts * cqty, 1);
|
||
}
|
||
});
|
||
syncUnit(
|
||
ctx,
|
||
code,
|
||
Math.floor(roundDown(sm, 0)),
|
||
Math.floor(roundDown(sl, 0)),
|
||
Math.floor(roundDown(ss, 0)),
|
||
);
|
||
xCount++;
|
||
});
|
||
|
||
// Step 2: D* 단가산출근거 65종 (COSTN.SMT) 연쇄 재계산
|
||
let dCount = 0;
|
||
Object.keys(memos).forEach((code) => {
|
||
const txt =
|
||
(ctx.uiState.editedSmtMap && ctx.uiState.editedSmtMap[code]) ||
|
||
(memos[code] && memos[code].text) ||
|
||
"";
|
||
if (!txt.trim()) return;
|
||
const ev = evaluateSmtFormulaText(txt, ctx, code);
|
||
if (ev.sumT > 0 || (byCodeZeroOk(crows, code) && ev.sumT === 0)) {
|
||
syncUnit(ctx, code, ev.sumM, ev.sumL, ev.sumS);
|
||
dCount++;
|
||
}
|
||
});
|
||
|
||
// Step 3: B* 일위대가 45종 연쇄 재계산
|
||
const ilwiRecs = recipes.filter((r) => r["종류"] === "일위대가");
|
||
let bCount = 0;
|
||
crows
|
||
.filter((r) =>
|
||
String(r.CODE || "")
|
||
.trim()
|
||
.startsWith("B"),
|
||
)
|
||
.forEach((br) => {
|
||
const code = String(br.CODE || "").trim();
|
||
const nm = String(br.NAME || "").trim();
|
||
const sz = String(br.SIZE || "").trim();
|
||
const rec =
|
||
ilwiRecs.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) ||
|
||
ilwiRecs.find((r) => r["명칭"].trim() === nm);
|
||
if (!rec || !rec["구성"]) return;
|
||
let sm = 0,
|
||
sl = 0,
|
||
ss = 0;
|
||
rec["구성"].forEach((comp) => {
|
||
const cnm = String(comp["명칭"] || "").trim();
|
||
const csz = String(comp["규격"] || "").trim();
|
||
const cunit = String(comp["단위"] || "").trim();
|
||
const cqty = num(comp["수량"], 0);
|
||
if (cqty === 0) return;
|
||
if (cunit === "%") {
|
||
const baseV = csz.includes("재료비") ? sm : sl;
|
||
if (cnm.includes("경비") || cnm.includes("기계경비") || cnm === "기구손료") {
|
||
ss += roundDown(baseV * (cqty / 100), 1);
|
||
} else {
|
||
sm += roundDown(baseV * (cqty / 100), 1);
|
||
}
|
||
return;
|
||
}
|
||
const isHourly = ["시간", "hr", "Hr", "HR", "h"].includes(cunit);
|
||
const cand = isHourly
|
||
? crows.find(
|
||
(x) => x.CODE.startsWith("X") && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||
) || crows.find((x) => x.CODE.startsWith("X") && x.NAME.trim() === cnm)
|
||
: crows.find(
|
||
(x) => x.CODE !== code && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||
) || crows.find((x) => x.CODE !== code && x.NAME.trim() === cnm);
|
||
if (cand) {
|
||
sm += roundDown(num(cand.MAMT1) * cqty, 1);
|
||
sl += roundDown(num(cand.LAMT1) * cqty, 1);
|
||
ss += roundDown(num(cand.SAMT1) * cqty, 1);
|
||
}
|
||
});
|
||
syncUnit(
|
||
ctx,
|
||
code,
|
||
Math.floor(roundDown(sm, 0)),
|
||
Math.floor(roundDown(sl, 0)),
|
||
Math.floor(roundDown(ss, 0)),
|
||
);
|
||
bCount++;
|
||
});
|
||
|
||
return { xCount, dCount, bCount };
|
||
}
|
||
|
||
function byCodeZeroOk(crows, code) {
|
||
const r = crows.find((x) => String(x.CODE || "").trim() === code);
|
||
return r && num(r.TAMT1) === 0;
|
||
}
|
||
|
||
/**
|
||
* 3. 선택 호표(B/X)의 표준 레시피 구성행 조회
|
||
*/
|
||
function getRecipeBreakdown(ctx, code) {
|
||
const crows = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||
const row = crows.find((r) => String(r.CODE || "").trim() === code);
|
||
if (!row) return [];
|
||
const nm = String(row.NAME || "").trim();
|
||
const sz = String(row.SIZE || "").trim();
|
||
const p = code[0].toUpperCase();
|
||
const recipes = (ctx.catalogData && ctx.catalogData.recipes) || [];
|
||
const kind = p === "X" ? "중기" : p === "B" ? "일위대가" : "단가산출";
|
||
const pool = recipes.filter((r) => r["종류"] === kind);
|
||
const rec =
|
||
pool.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) ||
|
||
pool.find((r) => r["명칭"].trim() === nm);
|
||
return rec && rec["구성"] ? rec["구성"] : [];
|
||
}
|
||
|
||
/**
|
||
* 4. 다중 시트 Excel SpreadsheetML (.xls) 통합 워크북 생성기 (갑지·원가계산서·설계내역서·일위대가·단가산출·중기·단가목록)
|
||
*/
|
||
function buildMultiSheetExcelXml(ctx) {
|
||
const esc = (v) =>
|
||
String(v ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
const kj = ctx.computed.kanjubComputed;
|
||
const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx);
|
||
const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||
const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {};
|
||
|
||
const mkSheet = (name, headers, rows) => `
|
||
<Worksheet ss:Name="${esc(name)}">
|
||
<Table>
|
||
<Row>${headers.map((h) => `<Cell><Data ss:Type="String">${esc(h)}</Data></Cell>`).join("")}</Row>
|
||
${rows
|
||
.map(
|
||
(r) =>
|
||
`<Row>${r
|
||
.map((c) => {
|
||
const isN = typeof c === "number" && Number.isFinite(c);
|
||
return `<Cell><Data ss:Type="${isN ? "Number" : "String"}">${esc(c)}</Data></Cell>`;
|
||
})
|
||
.join("")}</Row>`,
|
||
)
|
||
.join("\n")}
|
||
</Table>
|
||
</Worksheet>`;
|
||
|
||
const shWon = mkSheet(
|
||
"공사원가계산서",
|
||
["비목", "산정기초(원)", "요율(%)", "금액(원)", "산출근거"],
|
||
kj.lines.map((l) => [l.label, l.base || 0, l.rate || 0, l.amount || 0, l.formula || ""]),
|
||
);
|
||
const shBoq = mkSheet(
|
||
"설계내역서",
|
||
[
|
||
"공종번호",
|
||
"코드",
|
||
"공종명",
|
||
"규격",
|
||
"단위",
|
||
"수량",
|
||
"재료비단가",
|
||
"노무비단가",
|
||
"경비단가",
|
||
"합계단가",
|
||
"합계금액",
|
||
],
|
||
boqRows.map((r) => [
|
||
r.ITNUM || "",
|
||
r.BDCODE1 || "",
|
||
r.NAME1 || "",
|
||
r.SIZE1 || "",
|
||
r.UNIT1 || "",
|
||
num(r.QTY1),
|
||
num(r.MAMT1),
|
||
num(r.LAMT1),
|
||
num(r.SAMT1),
|
||
num(r.TAMT1),
|
||
num(r.T_AMT1),
|
||
]),
|
||
);
|
||
const shIlwi = mkSheet(
|
||
"일위대가목록표",
|
||
["코드", "품명", "규격", "단위", "재료비", "노무비", "경비", "합계단가"],
|
||
costnRows
|
||
.filter((r) => String(r.CODE || "").startsWith("B"))
|
||
.map((r) => [
|
||
r.CODE,
|
||
r.NAME,
|
||
r.SIZE,
|
||
r.UNIT,
|
||
num(r.MAMT1),
|
||
num(r.LAMT1),
|
||
num(r.SAMT1),
|
||
num(r.TAMT1),
|
||
]),
|
||
);
|
||
const shSan = mkSheet(
|
||
"단가산출근거",
|
||
[
|
||
"코드",
|
||
"품명",
|
||
"규격",
|
||
"단위",
|
||
"재료비",
|
||
"노무비",
|
||
"경비",
|
||
"합계단가",
|
||
"산출근거(COSTN.SMT)",
|
||
],
|
||
costnRows
|
||
.filter((r) => String(r.CODE || "").startsWith("D") || memos[r.CODE])
|
||
.map((r) => [
|
||
r.CODE,
|
||
r.NAME,
|
||
r.SIZE,
|
||
r.UNIT,
|
||
num(r.MAMT1),
|
||
num(r.LAMT1),
|
||
num(r.SAMT1),
|
||
num(r.TAMT1),
|
||
memos[r.CODE] ? memos[r.CODE].text.replace(/\r?\n/g, " | ") : "",
|
||
]),
|
||
);
|
||
const shJoong = mkSheet(
|
||
"중기사용료",
|
||
[
|
||
"코드",
|
||
"장비명",
|
||
"규격",
|
||
"단위",
|
||
"재료비(연료/잡품)",
|
||
"노무비(운전원)",
|
||
"경비(손료)",
|
||
"시간당사용료",
|
||
],
|
||
costnRows
|
||
.filter((r) => String(r.CODE || "").startsWith("X"))
|
||
.map((r) => [
|
||
r.CODE,
|
||
r.NAME,
|
||
r.SIZE,
|
||
r.UNIT,
|
||
num(r.MAMT1),
|
||
num(r.LAMT1),
|
||
num(r.SAMT1),
|
||
num(r.TAMT1),
|
||
]),
|
||
);
|
||
const shDan = mkSheet(
|
||
"단가목록표",
|
||
[
|
||
"코드",
|
||
"구분",
|
||
"품명",
|
||
"규격",
|
||
"단위",
|
||
"재료비",
|
||
"노무비",
|
||
"경비",
|
||
"적용단가",
|
||
"조달단가",
|
||
"물가자료",
|
||
],
|
||
costnRows
|
||
.filter((r) => /^[LMS]/.test(String(r.CODE || "")))
|
||
.map((r) => [
|
||
r.CODE,
|
||
r.CODE[0],
|
||
r.NAME,
|
||
r.SIZE,
|
||
r.UNIT,
|
||
num(r.MAMT1),
|
||
num(r.LAMT1),
|
||
num(r.SAMT1),
|
||
num(r.TAMT1),
|
||
num(r.AMT_11),
|
||
num(r.AMT_21),
|
||
]),
|
||
);
|
||
|
||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||
<?mso-application progid="Excel.Sheet"?>
|
||
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
||
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
||
${shWon}
|
||
${shBoq}
|
||
${shIlwi}
|
||
${shSan}
|
||
${shJoong}
|
||
${shDan}
|
||
</Workbook>`;
|
||
}
|
||
|
||
/**
|
||
* 5. 조달청 공사원가 호환규정 V1.0.5 (Valid_Xml.vsl · T3_C3B) 전체 계층 XML 생성기
|
||
*/
|
||
function buildFullPpsXml(ctx) {
|
||
const esc = (v) =>
|
||
String(v ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">");
|
||
const kj = ctx.computed.kanjubComputed;
|
||
const bt = ctx.computed.boqComputed.totals;
|
||
const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx);
|
||
const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||
|
||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||
<T3_C3B xmlns="http://www.pps.go.kr/cost/v105" schemaVersion="1.0.5">
|
||
<Header>
|
||
<Program>STMATE w 8.0 (경영정보)</Program>
|
||
<ProjectCode>${esc(ctx.state.projectMeta.pcode)}</ProjectCode>
|
||
<ProjectName>${esc(ctx.state.projectMeta.pname)}</ProjectName>
|
||
<PriceStandard>${esc(ctx.state.projectMeta.pnum)}</PriceStandard>
|
||
<DirectMaterial>${bt.directM}</DirectMaterial>
|
||
<DirectLabor>${bt.directL}</DirectLabor>
|
||
<DirectExpense>${bt.directS}</DirectExpense>
|
||
<DirectCostTotal>${bt.directT}</DirectCostTotal>
|
||
<TotalSupplyAmount>${kj.FINAL_SUPPLY}</TotalSupplyAmount>
|
||
<GovMaterialAmount>${kj.GOV_MAT}</GovMaterialAmount>
|
||
<GrandTotalCost>${kj.GRAND_TOTAL}</GrandTotalCost>
|
||
</Header>
|
||
<CostSummary>
|
||
${kj.lines
|
||
.map(
|
||
(ln) =>
|
||
` <CostLine name="${esc(ln.label)}" base="${ln.base || 0}" rate="${ln.rate || 0}" amount="${ln.amount || 0}" />`,
|
||
)
|
||
.join("\n")}
|
||
</CostSummary>
|
||
<BoqItems count="${boqRows.length}">
|
||
${boqRows
|
||
.map(
|
||
(r) =>
|
||
` <BoqRow recno="${r._recno}" itnum="${esc(r.ITNUM)}" code="${esc(r.BDCODE1)}" name="${esc(r.NAME1)}" spec="${esc(r.SIZE1)}" unit="${esc(r.UNIT1)}" qty="${num(r.QTY1)}" mUnit="${num(r.MAMT1)}" lUnit="${num(r.LAMT1)}" sUnit="${num(r.SAMT1)}" tAmt="${num(r.T_AMT1)}" />`,
|
||
)
|
||
.join("\n")}
|
||
</BoqItems>
|
||
<UnitCostCatalog count="${costnRows.length}">
|
||
${costnRows
|
||
.map(
|
||
(c) =>
|
||
` <CostItem code="${esc(c.CODE)}" jdcode="${esc(c.JDCODE)}" name="${esc(c.NAME)}" spec="${esc(c.SIZE)}" unit="${esc(c.UNIT)}" m="${num(c.MAMT1)}" l="${num(c.LAMT1)}" s="${num(c.SAMT1)}" t="${num(c.TAMT1)}" />`,
|
||
)
|
||
.join("\n")}
|
||
</UnitCostCatalog>
|
||
</T3_C3B>`;
|
||
}
|
||
|
||
global.STMATE_ENGINE = {
|
||
evaluateSmtFormulaText,
|
||
cascadeRecalculateAll,
|
||
getRecipeBreakdown,
|
||
buildMultiSheetExcelXml,
|
||
buildFullPpsXml,
|
||
};
|
||
})(window);
|