feat(stmate-test): 100% COSTN.SMT interpreter, 5-tier X/D/B/BOQ/Kanjub cascade engine, 30-sheet reports & 6-sheet Excel/XML export
This commit is contained in:
+137
-165
@@ -10,7 +10,12 @@
|
||||
const ViewDanIlwi = global.STMATE_VIEW_DAN_ILWI;
|
||||
const ViewKanjubPrt = global.STMATE_VIEW_KANJUB_PRT;
|
||||
const { fmt, num } = Calc;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const $ = (id) => document.getElementById(id),
|
||||
closeModal = () => $("stmate-modal-backdrop").classList.add("hidden"),
|
||||
onClick = (id, fn) => $(id) && ($(id).onclick = fn),
|
||||
onChange = (id, fn) => $(id) && ($(id).onchange = fn),
|
||||
bindEach = (sel, ev, fn) =>
|
||||
document.querySelectorAll(sel).forEach((el) => (el[ev] = (e) => fn(el, e)));
|
||||
|
||||
const ctx = {
|
||||
state: Calc.createDefaultState(),
|
||||
@@ -42,19 +47,16 @@
|
||||
},
|
||||
};
|
||||
|
||||
function recompute() {
|
||||
ctx.computed = Calc.computeAll(ctx.state);
|
||||
}
|
||||
const recompute = () => (ctx.computed = Calc.computeAll(ctx.state));
|
||||
|
||||
function renderChromeBars() {
|
||||
const prjBar = $("prj-context-bar");
|
||||
const stBar = $("stmate-statusbar");
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
const prjBar = $("prj-context-bar"),
|
||||
stBar = $("stmate-statusbar");
|
||||
const kj = ctx.computed.kanjubComputed,
|
||||
bt = ctx.computed.boqComputed.totals;
|
||||
const dbfInfo = ctx.dbfData
|
||||
? `COSTN ${ctx.dbfData.costn_count}건 · SMT산식 ${Object.keys(ctx.dbfData.costn_memos || {}).length}건 · ${ctx.dbfData.boq_table_name} ${ctx.dbfData.boq_count}행`
|
||||
: "내장 봉화군 2024 골든셋";
|
||||
|
||||
if (prjBar) {
|
||||
prjBar.innerHTML = `<div class="prj-context-left"><span class="prj-tag">열린 공사</span><strong>${ctx.state.projectMeta.pname}</strong><span>(${ctx.state.projectMeta.pnum || "2024년 단가"} · 코드 BOQ${ctx.state.projectMeta.pcode || "11"})</span></div><div class="prj-context-right"><span>원본 DBF/SMT: <strong>${dbfInfo}</strong></span><span>원가양식: <strong>${ctx.state.kanjubConfig.scheme}형식</strong></span></div>`;
|
||||
}
|
||||
@@ -117,17 +119,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function cascadeAndRender() {
|
||||
if (global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll) {
|
||||
global.STMATE_ENGINE.cascadeRecalculateAll(ctx);
|
||||
}
|
||||
renderWorkspace();
|
||||
}
|
||||
|
||||
async function saveAllToDbfServer() {
|
||||
if (!ctx.currentSourcePath || !ctx.dbfData) {
|
||||
return alert("먼저 PC 또는 실무 STMATE 프로젝트를 로드하세요.");
|
||||
}
|
||||
const stat =
|
||||
global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll
|
||||
? global.STMATE_ENGINE.cascadeRecalculateAll(ctx)
|
||||
: { xCount: 0, dCount: 0, bCount: 0 };
|
||||
recompute();
|
||||
renderChromeBars();
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
try {
|
||||
const saved = await postJson("/api/save", ViewPrjBoq.buildSavePayload(ctx));
|
||||
renderWorkspace();
|
||||
alert(
|
||||
`[계산완료 및 DBF/SMT/STC 저장 성공]\n• 폴더: ${saved.saved_dir}\n• 아카이브: ${saved.saved_stc} (${fmt(saved.stc_size)}B)\n• 직공비: ${fmt(bt.directT)}원 / 총공사비: ${fmt(kj.GRAND_TOTAL)}원`,
|
||||
`[계산완료(Act_Calc_All) 및 DBF/SMT/STC 저장 성공]\n• 5단 연쇄재계산: 중기(X) ${stat.xCount}건 · 산근(D) ${stat.dCount}건 · 일위대가(B) ${stat.bCount}건\n• 폴더: ${saved.saved_dir}\n• 아카이브: ${saved.saved_stc} (${fmt(saved.stc_size)}B)\n• 직공비: ${fmt(bt.directT)}원 / 총공사비: ${fmt(kj.GRAND_TOTAL)}원`,
|
||||
);
|
||||
} catch (err) {
|
||||
alert("DBF/STC 저장 실패: " + err.message);
|
||||
@@ -141,18 +156,6 @@
|
||||
bindModalEvents();
|
||||
}
|
||||
|
||||
const closeModal = () => $("stmate-modal-backdrop").classList.add("hidden");
|
||||
const onClick = (id, fn) => {
|
||||
const el = $(id);
|
||||
if (el) el.onclick = fn;
|
||||
};
|
||||
const onChange = (id, fn) => {
|
||||
const el = $(id);
|
||||
if (el) el.onchange = fn;
|
||||
};
|
||||
const bindEach = (sel, ev, fn) =>
|
||||
document.querySelectorAll(sel).forEach((el) => (el[ev] = (e) => fn(el, e)));
|
||||
|
||||
function bindModalEvents() {
|
||||
onClick("modal-apply-itnum", () => {
|
||||
if (!ctx.dbfData || !ctx.dbfData.boq_rows) return;
|
||||
@@ -175,9 +178,8 @@
|
||||
onClick("modal-apply-boqrate", () => {
|
||||
const [rm, rl, rs] = ["m", "l", "s"].map((k) => num($(`boqrate-${k}`).value, 100) / 100);
|
||||
ctx.state.resources.forEach((r) => {
|
||||
r.manualPrice = Math.floor(
|
||||
num(r.manualPrice) * (r.kind === "재료비" ? rm : r.kind === "노무비" ? rl : rs),
|
||||
);
|
||||
const mult = r.kind === "재료비" ? rm : r.kind === "노무비" ? rl : rs;
|
||||
r.manualPrice = Math.floor(num(r.manualPrice) * mult);
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
@@ -191,34 +193,16 @@
|
||||
const code = ($("expnd-code") || {}).value;
|
||||
const ilwi =
|
||||
(ctx.state.ilwiItems || []).find((x) => x.code === code) || ctx.state.ilwiItems[0];
|
||||
if (ilwi && ilwi.lines && ilwi.lines.length) {
|
||||
ilwi.lines.forEach((ln, idx) => {
|
||||
const r = ctx.state.resources.find((x) => x.id === ln.refId || x.code === ln.refId);
|
||||
const recno = (Date.now() % 90000) + idx + 1;
|
||||
ctx.state.boqItems.push({
|
||||
id: "BOQ_" + recno,
|
||||
code: r ? r.code : ln.refId,
|
||||
name: `[전개] ${r ? r.name : ln.refId}`,
|
||||
refId: ln.refId,
|
||||
rowType: "normal",
|
||||
qty: ln.qty || 1,
|
||||
changedQty: ln.qty || 1,
|
||||
_recno: recno,
|
||||
});
|
||||
if (ctx.dbfData && ctx.dbfData.boq_rows) {
|
||||
ctx.dbfData.boq_rows.splice(5 + idx, 0, {
|
||||
_recno: recno,
|
||||
ITNUM: "",
|
||||
BDCODE1: r ? r.code : ln.refId,
|
||||
NAME1: `[전개] ${r ? r.name : ln.refId}`,
|
||||
SIZE1: r ? r.spec : "",
|
||||
UNIT1: r ? r.unit : "식",
|
||||
QTY1: String(ln.qty || 1),
|
||||
ENDCODE: "0.0000",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
((ilwi && ilwi.lines) || []).forEach((ln, idx) => {
|
||||
const r = ctx.state.resources.find((x) => x.id === ln.refId || x.code === ln.refId);
|
||||
insertBoqRow(
|
||||
r ? r.code : ln.refId,
|
||||
`[전개] ${r ? r.name : ln.refId}`,
|
||||
r ? r.spec : "",
|
||||
r ? r.unit : "식",
|
||||
ln.qty || 1,
|
||||
);
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
});
|
||||
@@ -242,24 +226,26 @@
|
||||
(ctx.dbfData ? ctx.dbfData.costn_rows : []).forEach((r) => {
|
||||
const code = String(r.CODE || "").trim();
|
||||
if (!code.startsWith("M")) return;
|
||||
const cands = [num(r.AMT_11), num(r.AMT_21), num(r.AMT_31), num(r.AMT_41), num(r.AMT_51)];
|
||||
const nonZero = cands.filter((x) => x > 0);
|
||||
if (nonZero.length)
|
||||
ViewPrjBoq.syncCostnUnitPrice(
|
||||
ctx,
|
||||
code,
|
||||
mode === "min" ? Math.min(...nonZero) : cands[parseInt(mode, 10)] || nonZero[0],
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const nonZero = [
|
||||
num(r.AMT_11),
|
||||
num(r.AMT_21),
|
||||
num(r.AMT_31),
|
||||
num(r.AMT_41),
|
||||
num(r.AMT_51),
|
||||
].filter((x) => x > 0);
|
||||
if (nonZero.length) {
|
||||
const picked =
|
||||
mode === "min" ? Math.min(...nonZero) : nonZero[parseInt(mode, 10)] || nonZero[0];
|
||||
ViewPrjBoq.syncCostnUnitPrice(ctx, code, picked, 0, 0);
|
||||
}
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
onClick("modal-apply-edthal", () => {
|
||||
const kind = $("edthal-kind").value;
|
||||
const pct = num($("edthal-pct").value, 100) / 100;
|
||||
const rmode = $("edthal-round").value;
|
||||
const kind = $("edthal-kind").value,
|
||||
pct = num($("edthal-pct").value, 100) / 100,
|
||||
rmode = $("edthal-round").value;
|
||||
(ctx.dbfData ? ctx.dbfData.costn_rows : []).forEach((r) => {
|
||||
const code = String(r.CODE || "").trim();
|
||||
if (!code || r.UC === "T" || (kind !== "ALL" && code[0].toUpperCase() !== kind)) return;
|
||||
@@ -273,7 +259,7 @@
|
||||
);
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
bindEach(".btn-use-special-san", "onclick", (btn) => {
|
||||
const spec = (ctx.catalogData.special_san || [])[parseInt(btn.getAttribute("data-idx"), 10)];
|
||||
@@ -287,6 +273,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
function insertBoqRow(code, name, spec, unit, qty) {
|
||||
const recno = Date.now() % 100000;
|
||||
ctx.state.boqItems.push({
|
||||
id: "BOQ_" + recno,
|
||||
code,
|
||||
name,
|
||||
refId: code,
|
||||
rowType: "normal",
|
||||
qty,
|
||||
changedQty: qty,
|
||||
_recno: recno,
|
||||
});
|
||||
if (ctx.dbfData && ctx.dbfData.boq_rows) {
|
||||
ctx.dbfData.boq_rows.splice(4, 0, {
|
||||
_recno: recno,
|
||||
ITNUM: "",
|
||||
BDCODE1: code,
|
||||
NAME1: name,
|
||||
SIZE1: spec,
|
||||
UNIT1: unit,
|
||||
QTY1: String(qty),
|
||||
ENDCODE: "0.0000",
|
||||
});
|
||||
}
|
||||
renderWorkspace();
|
||||
}
|
||||
|
||||
function bindWorkspaceEvents() {
|
||||
const goScreen = (scr) => {
|
||||
ctx.uiState.activeScreen = scr;
|
||||
@@ -304,10 +317,15 @@
|
||||
const p = btn.getAttribute("data-path");
|
||||
p === ctx.currentSourcePath ? goScreen("wBoq") : loadProjectFromServer(p, "wPrjList");
|
||||
});
|
||||
onClick("prj-btn-open-boq", () => goScreen("wBoq"));
|
||||
onClick("prj-btn-kanjub", () => goScreen("wM_KanJub"));
|
||||
onClick("prj-btn-copy-reg", () => goScreen("wStage"));
|
||||
onClick("prj-btn-pack-stc", () => saveAllToDbfServer());
|
||||
"prj-btn-open-boq:wBoq,prj-btn-kanjub:wM_KanJub,prj-btn-copy-reg:wStage,kj-btn-open-rate:wM_KAN_RATE"
|
||||
.split(",")
|
||||
.forEach((p) => {
|
||||
const [id, scr] = p.split(":");
|
||||
onClick(id, () => goScreen(scr));
|
||||
});
|
||||
["prj-btn-pack-stc", "boq-btn-save-dbf", "kj-btn-save-dbf"].forEach((id) =>
|
||||
onClick(id, saveAllToDbfServer),
|
||||
);
|
||||
onClick("prj-btn-toggle-preview", () => {
|
||||
ctx.uiState.showBoqPreview = !ctx.uiState.showBoqPreview;
|
||||
renderWorkspace();
|
||||
@@ -368,33 +386,6 @@
|
||||
ctx.uiState.activePocket = parseInt(btn.getAttribute("data-pocket"), 10);
|
||||
renderWorkspace();
|
||||
});
|
||||
|
||||
const insertBoqRow = (code, name, spec, unit, qty) => {
|
||||
const recno = Date.now() % 100000;
|
||||
ctx.state.boqItems.push({
|
||||
id: "BOQ_" + recno,
|
||||
code,
|
||||
name,
|
||||
refId: code,
|
||||
rowType: "normal",
|
||||
qty,
|
||||
changedQty: qty,
|
||||
_recno: recno,
|
||||
});
|
||||
if (ctx.dbfData && ctx.dbfData.boq_rows) {
|
||||
ctx.dbfData.boq_rows.splice(4, 0, {
|
||||
_recno: recno,
|
||||
ITNUM: "",
|
||||
BDCODE1: code,
|
||||
NAME1: name,
|
||||
SIZE1: spec,
|
||||
UNIT1: unit,
|
||||
QTY1: String(qty),
|
||||
ENDCODE: "0.0000",
|
||||
});
|
||||
}
|
||||
renderWorkspace();
|
||||
};
|
||||
bindEach(".pocket-insert-btn", "onclick", (btn) => {
|
||||
const item =
|
||||
ctx.uiState.boqPockets[ctx.uiState.activePocket || 1][
|
||||
@@ -402,16 +393,12 @@
|
||||
];
|
||||
if (item) insertBoqRow(item.code, item.name, "", "식", item.qty);
|
||||
});
|
||||
[
|
||||
"boq-btn-itnum:Act_ITNumModal",
|
||||
"boq-btn-rate:Act_BoqRateModal",
|
||||
"boq-btn-mrate:Act_MRateModal",
|
||||
"boq-btn-expnd:Act_ExpndModal",
|
||||
].forEach((pair) => {
|
||||
const [id, key] = pair.split(":");
|
||||
bindModalOpener(id, key);
|
||||
});
|
||||
onClick("boq-btn-save-dbf", () => saveAllToDbfServer());
|
||||
"boq-btn-itnum:Act_ITNumModal,boq-btn-rate:Act_BoqRateModal,boq-btn-mrate:Act_MRateModal,boq-btn-expnd:Act_ExpndModal,dan-btn-boxa:Act_BoxaModal,dan-btn-edthal:Act_EdtHalModal,dan-btn-catalog:Act_CatalogModal,ilwi-btn-load-recipe:Act_CatalogModal"
|
||||
.split(",")
|
||||
.forEach((p) => {
|
||||
const [id, key] = p.split(":");
|
||||
bindModalOpener(id, key);
|
||||
});
|
||||
onClick("boq-btn-add-leaf", () => {
|
||||
const f = ctx.state.ilwiItems[0] || {
|
||||
code: "D00014",
|
||||
@@ -421,7 +408,6 @@
|
||||
};
|
||||
insertBoqRow(f.code, f.rawName || f.name, f.spec || "", f.unit || "m3", 10);
|
||||
});
|
||||
|
||||
onClick("sys-btn-apply", () => {
|
||||
Object.assign(ctx.state.sysConfig, {
|
||||
usdRate1: num($("sys-usd1").value, 1350),
|
||||
@@ -433,7 +419,7 @@
|
||||
rxFormula: $("sys-rx-formula").value,
|
||||
condCodes: $("sys-cond-codes").value,
|
||||
});
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
bindEach(".dan-filter-btn", "onclick", (btn) => {
|
||||
ctx.uiState.danFilter = btn.getAttribute("data-filter");
|
||||
@@ -454,21 +440,12 @@
|
||||
num(tr.querySelector(".dan-edit-l").value),
|
||||
num(tr.querySelector(".dan-edit-s").value),
|
||||
);
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
bindEach(".dan-jump-ilwi", "onclick", (btn) => {
|
||||
ctx.uiState.selectedIlwiCode = btn.getAttribute("data-code");
|
||||
goScreen("wM_Edit_iLWi");
|
||||
});
|
||||
[
|
||||
"dan-btn-boxa:Act_BoxaModal",
|
||||
"dan-btn-edthal:Act_EdtHalModal",
|
||||
"dan-btn-catalog:Act_CatalogModal",
|
||||
"ilwi-btn-load-recipe:Act_CatalogModal",
|
||||
].forEach((pair) => {
|
||||
const [id, key] = pair.split(":");
|
||||
bindModalOpener(id, key);
|
||||
});
|
||||
|
||||
bindEach(".ilwi-sub-btn", "onclick", (btn) => {
|
||||
ctx.uiState.ilwiSubFilter = btn.getAttribute("data-sub");
|
||||
@@ -493,10 +470,10 @@
|
||||
byteLen: txt.length,
|
||||
});
|
||||
}
|
||||
const evalRes = ViewDanIlwi.evaluateSmtFormulaText(txt, ctx);
|
||||
const evalRes = ViewDanIlwi.evaluateSmtFormulaText(txt, ctx, code);
|
||||
if (evalRes.sumT > 0)
|
||||
ViewPrjBoq.syncCostnUnitPrice(ctx, code, evalRes.sumM, evalRes.sumL, evalRes.sumS);
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
|
||||
bindEach(".kj-scheme-tab", "onclick", (btn) => {
|
||||
@@ -522,8 +499,6 @@
|
||||
renderWorkspace();
|
||||
}),
|
||||
);
|
||||
onClick("kj-btn-open-rate", () => goScreen("wM_KAN_RATE"));
|
||||
onClick("kj-btn-save-dbf", () => saveAllToDbfServer());
|
||||
onClick("kj-btn-apply-jbr", async () => {
|
||||
const jbrPath = $("kj-jbr-select").value;
|
||||
if (!jbrPath) return;
|
||||
@@ -533,7 +508,6 @@
|
||||
ctx.state.kanjubConfig.anjunBaseAdd = d.extracted_rates.anjunBaseAdd;
|
||||
renderWorkspace();
|
||||
});
|
||||
|
||||
bindEach(".kr-tab-btn", "onclick", (btn) => {
|
||||
ctx.uiState.kanRateTab = btn.getAttribute("data-tab");
|
||||
renderWorkspace();
|
||||
@@ -548,15 +522,15 @@
|
||||
renderWorkspace();
|
||||
}
|
||||
});
|
||||
|
||||
bindEach(".stage-tab-btn", "onclick", (btn) => {
|
||||
ctx.uiState.stageSubTab = btn.getAttribute("data-st");
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("st-btn-apply-bid", () => {
|
||||
ctx.uiState.bidRateM = num($("st-bid-m").value, 87.745);
|
||||
ctx.uiState.bidRateL = num($("st-bid-l").value, 87.745);
|
||||
ctx.uiState.bidRateS = num($("st-bid-s").value, 87.745);
|
||||
["M:m", "L:l", "S:s"].forEach((p) => {
|
||||
const [k, id] = p.split(":");
|
||||
ctx.uiState["bidRate" + k] = num($("st-bid-" + id).value, 87.745);
|
||||
});
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("st-btn-apply-gisung", () => {
|
||||
@@ -574,6 +548,14 @@
|
||||
ctx.uiState.activeReportSheet = e.target.value;
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("prt-btn-xls", () => {
|
||||
if (!global.STMATE_ENGINE) return;
|
||||
downloadBlob(
|
||||
global.STMATE_ENGINE.buildMultiSheetExcelXml(ctx),
|
||||
`STMATE_Workbook_${ctx.state.projectMeta.pcode || "11"}.xls`,
|
||||
"application/vnd.ms-excel;charset=utf-8",
|
||||
);
|
||||
});
|
||||
onClick("prt-btn-csv", () => {
|
||||
const tbl = document.querySelector("#mdi-workspace table.dbgrid");
|
||||
if (!tbl) return;
|
||||
@@ -589,12 +571,10 @@
|
||||
);
|
||||
});
|
||||
onClick("prt-btn-xml", () => {
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
downloadBlob(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n<T3_C3B xmlns="http://www.pps.go.kr/stmate">\n <ProjectName>${ctx.state.projectMeta.pname}</ProjectName>\n <DirectCost>${kj.DIRECT_COST}</DirectCost>\n <TotalSupply>${kj.FINAL_SUPPLY}</TotalSupply>\n <GrandTotal>${kj.GRAND_TOTAL}</GrandTotal>\n</T3_C3B>`,
|
||||
"PPS_CostExchange.xml",
|
||||
"application/xml",
|
||||
);
|
||||
const xmlStr = global.STMATE_ENGINE
|
||||
? global.STMATE_ENGINE.buildFullPpsXml(ctx)
|
||||
: "<T3_C3B />";
|
||||
downloadBlob(xmlStr, "PPS_CostExchange_V105.xml", "application/xml;charset=utf-8");
|
||||
});
|
||||
onClick("prt-btn-jdl", () => {
|
||||
const lines = (ctx.computed.boqComputed.rows || []).map(
|
||||
@@ -655,9 +635,8 @@
|
||||
ctx.uiState.activeScreen = btn.getAttribute("data-screen");
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("btn-calc-all", () => saveAllToDbfServer());
|
||||
onClick("modal-close-btn", () => closeModal());
|
||||
|
||||
onClick("btn-calc-all", saveAllToDbfServer);
|
||||
onClick("modal-close-btn", closeModal);
|
||||
bindEach("#main-menubar [data-act]", "onclick", (btn) => {
|
||||
const act = btn.getAttribute("data-act");
|
||||
if (MENU_SCREEN_MAP[act]) ctx.uiState.activeScreen = MENU_SCREEN_MAP[act];
|
||||
@@ -666,37 +645,27 @@
|
||||
return loadProjectFromServer(ctx.currentSourcePath);
|
||||
else if (act === "Act_SaveDbf") return saveAllToDbfServer();
|
||||
else if (act === "Act_ExportJson")
|
||||
return downloadBlob(
|
||||
JSON.stringify(ctx.state, null, 2),
|
||||
"stmate_snapshot.json",
|
||||
"application/json",
|
||||
);
|
||||
else if (act === "Act_DanList") {
|
||||
ctx.uiState.danFilter = btn.getAttribute("data-filter") || "ALL";
|
||||
ctx.uiState.activeScreen = "wM_JabPum";
|
||||
} else if (act === "Act_IlwiEdit") {
|
||||
ctx.uiState.ilwiSubFilter = btn.getAttribute("data-filter") || "ALL_BDX";
|
||||
ctx.uiState.activeScreen = "wM_Edit_iLWi";
|
||||
} else if (act === "Act_LoadGolden") {
|
||||
ctx.state = Calc.createDefaultState();
|
||||
ctx.dbfData = null;
|
||||
}
|
||||
return downloadBlob(JSON.stringify(ctx.state), "stmate_snapshot.json", "application/json");
|
||||
else if (act === "Act_DanList")
|
||||
Object.assign(ctx.uiState, {
|
||||
danFilter: btn.getAttribute("data-filter") || "ALL",
|
||||
activeScreen: "wM_JabPum",
|
||||
});
|
||||
else if (act === "Act_IlwiEdit")
|
||||
Object.assign(ctx.uiState, {
|
||||
ilwiSubFilter: btn.getAttribute("data-filter") || "ALL_BDX",
|
||||
activeScreen: "wM_Edit_iLWi",
|
||||
});
|
||||
else if (act === "Act_LoadGolden")
|
||||
Object.assign(ctx, { state: Calc.createDefaultState(), dbfData: null });
|
||||
renderWorkspace();
|
||||
});
|
||||
|
||||
onChange("json-file-input", (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
ctx.state = JSON.parse(String(reader.result));
|
||||
renderWorkspace();
|
||||
} catch (err) {
|
||||
alert("JSON 파싱 오류: " + err.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
const r = new FileReader();
|
||||
r.onload = () => ((ctx.state = JSON.parse(String(r.result))), renderWorkspace());
|
||||
r.readAsText(file);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -710,6 +679,9 @@
|
||||
.join("");
|
||||
sel.onchange = () => loadProjectFromServer(sel.value);
|
||||
const defaultPrj =
|
||||
ctx.sourcesData.projects.find(
|
||||
(p) => p.name.includes("[실무 STC]") && p.name.includes("기번41"),
|
||||
) ||
|
||||
ctx.sourcesData.projects.find((p) => p.name.includes("기번41")) ||
|
||||
ctx.sourcesData.projects[0];
|
||||
if (defaultPrj) return await loadProjectFromServer(defaultPrj.path, "wPrjList");
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
</div>
|
||||
|
||||
<script src="calc.js"></script>
|
||||
<script src="smt_engine.js"></script>
|
||||
<script src="view_prj_boq.js"></script>
|
||||
<script src="view_dan_ilwi.js"></script>
|
||||
<script src="view_kanjub_prt.js"></script>
|
||||
|
||||
+35
-20
@@ -160,7 +160,7 @@ def convert_dbf_to_sim_state(
|
||||
fnum(r.get("SAMT1")),
|
||||
)
|
||||
cands = [fnum(r.get(f"AMT_{i}1")) for i in range(1, 6)]
|
||||
amt61 = fnum(r.get("AMT_61")) or t1
|
||||
amt61 = t1 or fnum(r.get("AMT_61"))
|
||||
memo_obj = costn_memos.get(code)
|
||||
|
||||
if prefix in ("M", "L", "S"):
|
||||
@@ -585,37 +585,52 @@ def api_save(req: SaveDbfReq):
|
||||
"SIZE1",
|
||||
"UNIT1",
|
||||
"QTY1",
|
||||
"QTY2",
|
||||
"MAMT1",
|
||||
"LAMT1",
|
||||
"SAMT1",
|
||||
"TAMT1",
|
||||
"MAMT2",
|
||||
"LAMT2",
|
||||
"SAMT2",
|
||||
"TAMT2",
|
||||
"MAMT3",
|
||||
"LAMT3",
|
||||
"SAMT3",
|
||||
"TAMT3",
|
||||
):
|
||||
if k in u and k in r:
|
||||
r[k] = str(u[k])
|
||||
bd = (r.get("BDCODE1") or "").strip()
|
||||
if bd and not STRUCT_RE.match(bd):
|
||||
q = Decimal(str(fnum(r.get("QTY1"))))
|
||||
m_a = int(
|
||||
(q * Decimal(str(fnum(r.get("MAMT1"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
for s_idx in ("1", "2", "3"):
|
||||
q_k = (
|
||||
f"QTY{s_idx}" if f"QTY{s_idx}" in r and r.get(f"QTY{s_idx}") else "QTY1"
|
||||
)
|
||||
)
|
||||
l_a = int(
|
||||
(q * Decimal(str(fnum(r.get("LAMT1"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
q = Decimal(str(fnum(r.get(q_k))))
|
||||
m_a = int(
|
||||
(q * Decimal(str(fnum(r.get(f"MAMT{s_idx}"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
)
|
||||
)
|
||||
)
|
||||
s_a = int(
|
||||
(q * Decimal(str(fnum(r.get("SAMT1"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
l_a = int(
|
||||
(q * Decimal(str(fnum(r.get(f"LAMT{s_idx}"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
)
|
||||
)
|
||||
)
|
||||
r["M_AMT1"], r["L_AMT1"], r["S_AMT1"], r["T_AMT1"] = (
|
||||
str(m_a),
|
||||
str(l_a),
|
||||
str(s_a),
|
||||
str(m_a + l_a + s_a),
|
||||
)
|
||||
s_a = int(
|
||||
(q * Decimal(str(fnum(r.get(f"SAMT{s_idx}"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
)
|
||||
)
|
||||
for col_n, v_n in (
|
||||
(f"M_AMT{s_idx}", m_a),
|
||||
(f"L_AMT{s_idx}", l_a),
|
||||
(f"S_AMT{s_idx}", s_a),
|
||||
(f"T_AMT{s_idx}", m_a + l_a + s_a),
|
||||
):
|
||||
if col_n in r:
|
||||
r[col_n] = str(v_n)
|
||||
files[boq_name] = build_dbf_bytes(flds, rows, meta)
|
||||
|
||||
if "PRJLST.DBF" in files and req.prjlst_updates:
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -23,132 +23,11 @@
|
||||
/**
|
||||
* COSTN.SMT 텍스트를 실시간 평가하여 Q식, Cm식, 참조(&X00006L/M/S&) 단가 및 비목별 합계 산출
|
||||
*/
|
||||
function evaluateSmtFormulaText(smtText, ctx) {
|
||||
const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||||
const byCode = {};
|
||||
rawCostn.forEach((r) => {
|
||||
const c = String(r.CODE || "").trim();
|
||||
if (c) byCode[c] = r;
|
||||
});
|
||||
// state.resources / machines 동기화 반영
|
||||
(ctx.computed.resList || []).forEach((r) => {
|
||||
if (!r.isSynthetic && byCode[r.code]) {
|
||||
byCode[r.code].TAMT1 = r.appliedPrice;
|
||||
if (r.kind === "노무비") byCode[r.code].LAMT1 = r.appliedPrice;
|
||||
if (r.kind === "재료비") byCode[r.code].MAMT1 = r.appliedPrice;
|
||||
if (r.kind === "경비") byCode[r.code].SAMT1 = r.appliedPrice;
|
||||
}
|
||||
});
|
||||
|
||||
const vars = {};
|
||||
const linesOut = [];
|
||||
let sumL = 0,
|
||||
sumM = 0,
|
||||
sumS = 0;
|
||||
|
||||
function evalMathWithVars(exprStr) {
|
||||
let s = String(exprStr || "")
|
||||
.replace(/'[^']*'/g, "")
|
||||
.trim();
|
||||
if (!s) return 0;
|
||||
// 변수 치환 (긴 변수명 우선)
|
||||
Object.keys(vars)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.forEach((k) => {
|
||||
const re = new RegExp("\\b" + k + "\\b", "g");
|
||||
s = s.replace(re, String(vars[k]));
|
||||
});
|
||||
s = s.replace(/[^0-9.+\-*/()]/g, "");
|
||||
if (!s) return 0;
|
||||
try {
|
||||
const v = Function('"use strict"; return (' + s + ");")();
|
||||
return Number.isFinite(v) ? v : 0;
|
||||
} catch (_e) {
|
||||
return 0;
|
||||
}
|
||||
function evaluateSmtFormulaText(smtText, ctx, codeHint) {
|
||||
if (global.STMATE_ENGINE && global.STMATE_ENGINE.evaluateSmtFormulaText) {
|
||||
return global.STMATE_ENGINE.evaluateSmtFormulaText(smtText, ctx, codeHint);
|
||||
}
|
||||
|
||||
const rawLines = String(smtText || "").split(/\r?\n/);
|
||||
rawLines.forEach((rawLn, idx) => {
|
||||
const ln = rawLn.trim();
|
||||
if (!ln) return;
|
||||
|
||||
// 1) 변수 대입 (q=0.4 , K=0.7 , E=0.45-0.05= , f=1/1.175 , Cm=... , Q=...)
|
||||
const assignRe = /\b([A-Za-z]\w*)\s*(?:'[^']*')?\s*=\s*([^,\r\n'=]+)/g;
|
||||
let m;
|
||||
const assignedHere = [];
|
||||
while ((m = assignRe.exec(ln)) !== null) {
|
||||
const vName = m[1].trim();
|
||||
const vExpr = m[2].trim();
|
||||
const val = evalMathWithVars(vExpr);
|
||||
if (val > 0 || vExpr === "0") {
|
||||
vars[vName] = Math.round(val * 100) / 100;
|
||||
assignedHere.push(`${vName} = ${vars[vName]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 자원 참조 (&X00026L& / Q = , &M00080& * 0.00438'㎥' = ) 또는 주석형 단가 요약
|
||||
const refMatch = ln.match(/&([A-Z]\d{4,5})([LMS])?&(.*)/);
|
||||
const commentCostMatch = !refMatch && ln.match(/(재료비|노무비|경\s*비)\s*[:=]\s*([\d,]+)/);
|
||||
let refInfo = null;
|
||||
if (refMatch) {
|
||||
const refCode = refMatch[1];
|
||||
const compTag = refMatch[2] || refCode[0];
|
||||
const tailExpr = refMatch[3] || "";
|
||||
const targetRow = byCode[refCode] || {};
|
||||
let baseUnitPrice = num(targetRow.TAMT1);
|
||||
if (compTag === "L") baseUnitPrice = num(targetRow.LAMT1) || baseUnitPrice;
|
||||
else if (compTag === "M") baseUnitPrice = num(targetRow.MAMT1) || baseUnitPrice;
|
||||
else if (compTag === "S") baseUnitPrice = num(targetRow.SAMT1) || baseUnitPrice;
|
||||
|
||||
const cleanTail = tailExpr.split("=")[0].split(":=")[0];
|
||||
const fullExpr = String(baseUnitPrice) + " " + cleanTail;
|
||||
const lineAmt = evalMathWithVars(fullExpr);
|
||||
const roundedAmt = Math.floor(lineAmt * 10) / 10;
|
||||
|
||||
if (compTag === "L" || refCode.startsWith("L")) sumL += roundedAmt;
|
||||
else if (compTag === "S" || refCode.startsWith("S")) sumS += roundedAmt;
|
||||
else sumM += roundedAmt;
|
||||
|
||||
refInfo = {
|
||||
refCode,
|
||||
compTag,
|
||||
refName: targetRow.NAME || refCode,
|
||||
baseUnitPrice,
|
||||
calcAmt: roundedAmt,
|
||||
};
|
||||
} else if (commentCostMatch) {
|
||||
const cKind = commentCostMatch[1].replace(/\s+/g, "");
|
||||
const cVal = num(commentCostMatch[2], 0);
|
||||
const cTag = cKind === "노무비" ? "L" : cKind === "경비" ? "S" : "M";
|
||||
if (cTag === "L") sumL += cVal;
|
||||
else if (cTag === "S") sumS += cVal;
|
||||
else sumM += cVal;
|
||||
refInfo = {
|
||||
refCode: cKind,
|
||||
compTag: cTag,
|
||||
refName: "단가 직접지정",
|
||||
baseUnitPrice: cVal,
|
||||
calcAmt: cVal,
|
||||
};
|
||||
}
|
||||
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: assignedHere.join(", "),
|
||||
refInfo,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
vars,
|
||||
linesOut,
|
||||
sumM: Math.floor(sumM),
|
||||
sumL: Math.floor(sumL),
|
||||
sumS: Math.floor(sumS),
|
||||
sumT: Math.floor(sumM) + Math.floor(sumL) + Math.floor(sumS),
|
||||
};
|
||||
return { vars: {}, linesOut: [], sumM: 0, sumL: 0, sumS: 0, sumT: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,15 +259,28 @@
|
||||
|
||||
const selRow = bdxRows.find((r) => String(r.CODE).trim() === selCode) || {};
|
||||
const memoObj = memos[selCode] || null;
|
||||
const recipeComps =
|
||||
global.STMATE_ENGINE && global.STMATE_ENGINE.getRecipeBreakdown
|
||||
? global.STMATE_ENGINE.getRecipeBreakdown(ctx, selCode)
|
||||
: [];
|
||||
const defaultText = memoObj
|
||||
? memoObj.text
|
||||
: `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""})'\r\n'1. 재료비 : ${fmt(num(selRow.MAMT1))}원'\r\n'2. 노무비 : ${fmt(num(selRow.LAMT1))}원'\r\n'3. 경 비 : ${fmt(num(selRow.SAMT1))}원'\r\n(==)`;
|
||||
: recipeComps.length
|
||||
? `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""}) — 표준 호표 조립 구성 (${recipeComps.length}행)'\r\n` +
|
||||
recipeComps
|
||||
.map(
|
||||
(c, ci) =>
|
||||
`'${ci + 1}. ${c["명칭"]} (${c["규격"] || "-"}) : 수량 ${c["수량"]} ${c["단위"] || ""}'`,
|
||||
)
|
||||
.join("\r\n") +
|
||||
`\r\n'노 무 비 : ${num(selRow.LAMT1)}'\r\n'재 료 비 : ${num(selRow.MAMT1)}'\r\n'경 비 : ${num(selRow.SAMT1)}'\r\n(==)`
|
||||
: `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""})'\r\n'1. 재료비 : ${fmt(num(selRow.MAMT1))}원'\r\n'2. 노무비 : ${fmt(num(selRow.LAMT1))}원'\r\n'3. 경 비 : ${fmt(num(selRow.SAMT1))}원'\r\n(==)`;
|
||||
const currentSmtText =
|
||||
ctx.uiState.editedSmtMap && ctx.uiState.editedSmtMap[selCode] !== undefined
|
||||
? ctx.uiState.editedSmtMap[selCode]
|
||||
: defaultText;
|
||||
|
||||
const evalRes = evaluateSmtFormulaText(currentSmtText, ctx);
|
||||
const evalRes = evaluateSmtFormulaText(currentSmtText, ctx, selCode);
|
||||
|
||||
return `
|
||||
<div class="win-form">
|
||||
|
||||
@@ -456,39 +456,49 @@
|
||||
|
||||
<div class="win-subbar">
|
||||
<label>보고서 양식(SET_REP):</label>
|
||||
<select id="prt-sheet-select" class="win-select" style="width:240px;">
|
||||
<select id="prt-sheet-select" class="win-select" style="width:260px;">
|
||||
<option value="0608" ${activeRep === "0608" ? "selected" : ""}>[0608] 공사원가계산서 (PM_Won_A)</option>
|
||||
<option value="0601" ${activeRep === "0601" ? "selected" : ""}>[0601] 총괄설계내역서 (PM_Nae_Tot)</option>
|
||||
<option value="0603" ${activeRep === "0603" ? "selected" : ""}>[0603] 설계내역서 (PM_Nae)</option>
|
||||
<option value="0607" ${activeRep === "0607" ? "selected" : ""}>[0607] 관급자재내역서 (PM_Nae_Kwn)</option>
|
||||
<option value="0302" ${activeRep === "0302" ? "selected" : ""}>[0302] 일위대가표 / 목록표 (PM_iLWi)</option>
|
||||
<option value="0402" ${activeRep === "0402" ? "selected" : ""}>[0402] 단가산출근거 (Edit_SanKun)</option>
|
||||
<option value="0203" ${activeRep === "0203" ? "selected" : ""}>[0203] 시간당중기사용료 (PM_JOONG)</option>
|
||||
<option value="0643" ${activeRep === "0643" ? "selected" : ""}>[0643] 설계변경내역서/증감대비표 (CHANGE)</option>
|
||||
<option value="0623" ${activeRep === "0623" ? "selected" : ""}>[0623/0673] 계약내역서·실행예산서</option>
|
||||
<option value="0633" ${activeRep === "0633" ? "selected" : ""}>[0633] 기성부분내역서·수량조서</option>
|
||||
<option value="0302" ${activeRep === "0302" ? "selected" : ""}>[0301/0302] 일위대가표·목록표 (PM_iLWi)</option>
|
||||
<option value="0402" ${activeRep === "0402" ? "selected" : ""}>[0401/0402] 단가산출근거·목록표 (Edit_SanKun)</option>
|
||||
<option value="0203" ${activeRep === "0203" ? "selected" : ""}>[0202/0203] 시간당중기사용료·목록표 (PM_JOONG)</option>
|
||||
<option value="0101" ${activeRep === "0101" ? "selected" : ""}>[0101~0105] 노무·재료·경비·단가대비표</option>
|
||||
<option value="0501" ${activeRep === "0501" ? "selected" : ""}>[0501~0507] 자원/중기/대가 수량금액집계표</option>
|
||||
<option value="0201" ${activeRep === "0201" ? "selected" : ""}>[0201] 환율및기초자료 · 〓 INITIAL 〓</option>
|
||||
<option value="SET_REP_ALL" ${activeRep === "SET_REP_ALL" ? "selected" : ""}>[전체] SET_REP.DBF 75종 양식 설정표</option>
|
||||
</select>
|
||||
<button class="win-btn primary" id="prt-btn-csv">📊 현재 시트 CSV(엑셀호환) 다운로드</button>
|
||||
<button class="win-btn" id="prt-btn-xml">🏛️ 조달청 호환규정 XML 내보내기(uM_XML_Info)</button>
|
||||
<button class="win-btn" id="prt-btn-jdl">📄 조달청 JDL 전자내역 파일 내보내기</button>
|
||||
<button class="win-btn" onclick="window.print()">🖨️ 브라우저 인쇄</button>
|
||||
<button class="win-btn primary" id="prt-btn-xls">📗 다중시트 엑셀(.xls 워크북) 내보내기</button>
|
||||
<button class="win-btn" id="prt-btn-csv">📊 현재 시트 CSV 다운로드</button>
|
||||
<button class="win-btn" id="prt-btn-xml">🏛️ 조달청 호환규정 XML(T3_C3B)</button>
|
||||
<button class="win-btn" id="prt-btn-jdl">📄 조달청 JDL 내보내기</button>
|
||||
<button class="win-btn" onclick="window.print()">🖨️ 인쇄</button>
|
||||
</div>
|
||||
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;padding:16px;overflow:auto;background:#fff;">
|
||||
<h2 style="text-align:center;margin:0 0 6px;letter-spacing:4px;color:#0f294d;">
|
||||
${
|
||||
activeRep === "0608"
|
||||
? "공 사 원 가 계 산 서"
|
||||
: activeRep === "0603"
|
||||
? "설 계 내 역 서"
|
||||
: activeRep === "0607"
|
||||
? "관 급 자 재 내 역 서"
|
||||
: activeRep === "0302"
|
||||
? "일 위 대 가 표 (목 록)"
|
||||
: activeRep === "0402"
|
||||
? "단 가 산 출 근 거"
|
||||
: activeRep === "0203"
|
||||
? "시 간 당 중 기 사 용 료"
|
||||
: "자 재 · 노 무 · 경 비 단 가 목 록 표"
|
||||
{
|
||||
"0608": "공 사 원 가 계 산 서",
|
||||
"0601": "총 괄 설 계 내 역 서",
|
||||
"0603": "설 계 내 역 서",
|
||||
"0607": "관 급 자 재 내 역 서",
|
||||
"0643": "설 계 변 경 내 역 서 (증 감 대 비)",
|
||||
"0623": "계 약 내 역 서 · 실 행 예 산 서",
|
||||
"0633": "기 성 부 분 내 역 서 · 수 량 조 서",
|
||||
"0302": "일 위 대 가 표 (목 록)",
|
||||
"0402": "단 가 산 출 근 거",
|
||||
"0203": "시 간 당 중 기 사 용 료",
|
||||
"0101": "자 재 · 노 무 · 경 비 단 가 목 록 표",
|
||||
"0501": "자 원 · 중 기 · 대 가 수 량 금 액 집 계 표",
|
||||
"0201": "환 율 및 기 초 자 료 (〓 INITIAL 〓)",
|
||||
SET_REP_ALL: "SET_REP.DBF 75종 양식 설정표",
|
||||
}[activeRep] || "설 계 보 고 서"
|
||||
}
|
||||
</h2>
|
||||
<p style="text-align:center;margin:0 0 12px;font-weight:700;color:#334155;">
|
||||
@@ -518,12 +528,18 @@
|
||||
}
|
||||
|
||||
${
|
||||
activeRep === "0603" || activeRep === "0607"
|
||||
["0601", "0603", "0607", "0643", "0623", "0633"].includes(activeRep)
|
||||
? `<table class="dbgrid">
|
||||
<thead><tr><th>공종</th><th>명칭</th><th>규격</th><th>단위</th><th>수량</th><th>재료비단가</th><th>노무비단가</th><th>경비단가</th><th>합계단가</th><th>합계금액</th><th>비고</th></tr></thead>
|
||||
<tbody>
|
||||
${boqRows
|
||||
.filter((r) => (activeRep === "0607" ? r._isExcl : true))
|
||||
.filter((r) =>
|
||||
activeRep === "0607"
|
||||
? r._isExcl
|
||||
: activeRep === "0601"
|
||||
? /^(T\d|E\d|C\d)$/.test(String(r.BDCODE1 || "").trim())
|
||||
: true,
|
||||
)
|
||||
.map(
|
||||
(r) => `
|
||||
<tr class="${r._rowClass}">
|
||||
@@ -561,7 +577,7 @@
|
||||
}
|
||||
|
||||
${
|
||||
activeRep === "0302" || activeRep === "0203" || activeRep === "0101"
|
||||
["0302", "0203", "0101", "0501", "0201"].includes(activeRep)
|
||||
? `<table class="dbgrid">
|
||||
<thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>재료비</th><th>노무비</th><th>경비</th><th>합계단가</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -570,6 +586,7 @@
|
||||
const p = String(r.CODE || " ")[0].toUpperCase();
|
||||
if (activeRep === "0302") return p === "B" || p === "D";
|
||||
if (activeRep === "0203") return p === "X";
|
||||
if (activeRep === "0201") return p === "L" || p === "X";
|
||||
return p === "L" || p === "M" || p === "S";
|
||||
})
|
||||
.map(
|
||||
|
||||
@@ -480,85 +480,26 @@
|
||||
*/
|
||||
function buildBoqToolModalHtml(toolKind, ctx) {
|
||||
if (toolKind === "wITNum") {
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wITNum] 내역서 공종번호 자동입력</strong> — 대공종(T4)/중공종(T2)/말단 공종행에 번호체계를 일괄 부여합니다.</p>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);">
|
||||
<div class="kanjub-field"><label>대공종(T4) 번호형식</label><select id="itnum-t4-fmt"><option value="1.">1. / 2. / 3.</option><option value="I.">I. / II. / III.</option><option value="가.">가. / 나. / 다.</option></select></div>
|
||||
<div class="kanjub-field"><label>중공종(T2) 번호형식</label><select id="itnum-t2-fmt"><option value="1.1">1.1 / 1.2 / 1.3</option><option value="(1)">(1) / (2) / (3)</option></select></div>
|
||||
<div class="kanjub-field"><label>말단 공종행 번호부여</label><select id="itnum-leaf-fmt"><option value="none">공란 유지 (STMATE 기본)</option><option value="01">01, 02, 03...</option><option value="-1">-1, -2, -3...</option></select></div>
|
||||
</div>
|
||||
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-itnum">공종번호 일괄 부여 실행</button></div>
|
||||
</div>
|
||||
`;
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wITNum] 내역서 공종번호 자동입력</strong> — 대공종(T4)/중공종(T2)/말단 공종행에 번호체계를 일괄 부여합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>대공종(T4) 번호형식</label><select id="itnum-t4-fmt"><option value="1.">1. / 2. / 3.</option><option value="I.">I. / II. / III.</option><option value="가.">가. / 나. / 다.</option></select></div><div class="kanjub-field"><label>중공종(T2) 번호형식</label><select id="itnum-t2-fmt"><option value="1.1">1.1 / 1.2 / 1.3</option><option value="(1)">(1) / (2) / (3)</option></select></div><div class="kanjub-field"><label>말단 공종행 번호부여</label><select id="itnum-leaf-fmt"><option value="none">공란 유지 (STMATE 기본)</option><option value="01">01, 02, 03...</option><option value="-1">-1, -2, -3...</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-itnum">공종번호 일괄 부여 실행</button></div></div>`;
|
||||
}
|
||||
if (toolKind === "wBoqRate") {
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wBoqRate] 내역 항목 할증율 일괄 적용</strong> — 전체 또는 비목별(재·노·경) 할증 비율(%)을 적용합니다.</p>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="kanjub-field"><label>재료비 할증(%)</label><input type="number" id="boqrate-m" value="100" step="1" /></div>
|
||||
<div class="kanjub-field"><label>노무비 할증(%)</label><input type="number" id="boqrate-l" value="100" step="1" /></div>
|
||||
<div class="kanjub-field"><label>경비 할증(%)</label><input type="number" id="boqrate-s" value="100" step="1" /></div>
|
||||
</div>
|
||||
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-boqrate">내역 할증율 적용</button></div>
|
||||
</div>
|
||||
`;
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wBoqRate] 내역 항목 할증율 일괄 적용</strong> — 전체 또는 비목별(재·노·경) 할증 비율(%)을 적용합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>재료비 할증(%)</label><input type="number" id="boqrate-m" value="100" step="1" /></div><div class="kanjub-field"><label>노무비 할증(%)</label><input type="number" id="boqrate-l" value="100" step="1" /></div><div class="kanjub-field"><label>경비 할증(%)</label><input type="number" id="boqrate-s" value="100" step="1" /></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-boqrate">내역 할증율 적용</button></div></div>`;
|
||||
}
|
||||
if (toolKind === "wM_Rate") {
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wM_Rate] 선택 범위 수량·비목 단수 일괄 보정</strong> — 절사/반올림 및 소수점 자리수(0~4위)를 일괄 보정합니다.</p>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);">
|
||||
<div class="kanjub-field"><label>보정 방식</label><select id="mrate-mode"><option value="trunc">절사 (ROUNDDOWN)</option><option value="round">반올림 (ROUND)</option></select></div>
|
||||
<div class="kanjub-field"><label>소수 자리수(ECD)</label><select id="mrate-digits"><option value="0">소수 0위 (정수 1원)</option><option value="1">소수 1위 (0.1)</option><option value="2">소수 2위 (0.01)</option><option value="-1">10원 단위 절사</option></select></div>
|
||||
</div>
|
||||
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-mrate">단수 일괄 보정 적용</button></div>
|
||||
</div>
|
||||
`;
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wM_Rate] 선택 범위 수량·비목 단수 일괄 보정</strong> — 절사/반올림 및 소수점 자리수(0~4위)를 일괄 보정합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);"><div class="kanjub-field"><label>보정 방식</label><select id="mrate-mode"><option value="trunc">절사 (ROUNDDOWN)</option><option value="round">반올림 (ROUND)</option></select></div><div class="kanjub-field"><label>소수 자리수(ECD)</label><select id="mrate-digits"><option value="0">소수 0위 (정수 1원)</option><option value="1">소수 1위 (0.1)</option><option value="2">소수 2위 (0.01)</option><option value="-1">10원 단위 절사</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-mrate">단수 일괄 보정 적용</button></div></div>`;
|
||||
}
|
||||
if (toolKind === "wExpnd") {
|
||||
const bdxList = (ctx.state.ilwiItems || []).slice(0, 30);
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wExpnd / wM_Expnd] 일위대가·단가산출 내역서 풀어넣기</strong> — 선택한 호표의 재료·노무·경비 구성행을 내역서에 직접 전개합니다.</p>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="kanjub-field"><label>전개할 호표 선택</label><select id="expnd-code">${bdxList.map((x) => `<option value="${x.code}">${x.code} ${x.rawName || x.name}</option>`).join("")}</select></div>
|
||||
<div class="kanjub-field"><label>집계 구분선(RGrpToT)</label><select id="expnd-tot"><option value="E1">소계(=)</option><option value="E2">계(==)</option><option value="E3">합계(===)</option></select></div>
|
||||
<div class="kanjub-field"><label>단가 끝전 처리</label><select id="expnd-round"><option value="trunc">절삭하여 소수 0위 적용</option><option value="round">반올림하여 소수 0위 적용</option></select></div>
|
||||
</div>
|
||||
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-expnd">일위대가 풀어넣기 실행</button></div>
|
||||
</div>
|
||||
`;
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wExpnd / wM_Expnd] 일위대가·단가산출 내역서 풀어넣기</strong> — 선택한 호표의 재료·노무·경비 구성행을 내역서에 직접 전개합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>전개할 호표 선택</label><select id="expnd-code">${bdxList.map((x) => `<option value="${x.code}">${x.code} ${x.rawName || x.name}</option>`).join("")}</select></div><div class="kanjub-field"><label>집계 구분선(RGrpToT)</label><select id="expnd-tot"><option value="E1">소계(=)</option><option value="E2">계(==)</option><option value="E3">합계(===)</option></select></div><div class="kanjub-field"><label>단가 끝전 처리</label><select id="expnd-round"><option value="trunc">절삭하여 소수 0위 적용</option><option value="round">반올림하여 소수 0위 적용</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-expnd">일위대가 풀어넣기 실행</button></div></div>`;
|
||||
}
|
||||
// wQtyList (수량산출서 연결 집계)
|
||||
const leafRows = ctx.computed.boqComputed.rows || [];
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wQtyList] 수량산출서(QCODE.DBF) 코드별 수량 연결·집계</strong> — 수량산출식 결과를 내역서 QTY1에 즉시 반영합니다.</p>
|
||||
<div class="grid-scroll" style="max-height:320px;">
|
||||
<table class="dbgrid">
|
||||
<thead><tr><th>코드</th><th>공종명</th><th>현재 내역수량</th><th>산출근거 수식 (예: 3.46*1000)</th><th>반영</th></tr></thead>
|
||||
<tbody>
|
||||
${leafRows
|
||||
.slice(0, 25)
|
||||
.map(
|
||||
(r) => `
|
||||
<tr>
|
||||
<td class="center"><strong>${r.code}</strong></td>
|
||||
<td>${r.name}</td>
|
||||
<td class="num">${fmt(r.qty, 2)}</td>
|
||||
<td><input type="text" class="win-input qty-expr-input" data-id="${r.id}" value="${r.qty}" style="width:100%;" /></td>
|
||||
<td class="center"><button class="win-btn btn-apply-qty-expr" data-id="${r.id}">산식반영</button></td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wQtyList] 수량산출서(QCODE.DBF) 코드별 수량 연결·집계</strong> — 수량산출식 결과를 내역서 QTY1에 즉시 반영합니다.</p><div class="grid-scroll" style="max-height:320px;"><table class="dbgrid"><thead><tr><th>코드</th><th>공종명</th><th>현재 내역수량</th><th>산출근거 수식 (예: 3.46*1000)</th><th>반영</th></tr></thead><tbody>${leafRows
|
||||
.slice(0, 25)
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr><td class="center"><strong>${r.code}</strong></td><td>${r.name}</td><td class="num">${fmt(r.qty, 2)}</td><td><input type="text" class="win-input qty-expr-input" data-id="${r.id}" value="${r.qty}" style="width:100%;" /></td><td class="center"><button class="win-btn btn-apply-qty-expr" data-id="${r.id}">산식반영</button></td></tr>`,
|
||||
)
|
||||
.join("")}</tbody></table></div></div>`;
|
||||
}
|
||||
|
||||
function syncCostnUnitPrice(ctx, code, mVal, lVal, sVal) {
|
||||
@@ -610,20 +551,47 @@
|
||||
|
||||
function buildSavePayload(ctx) {
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const revKj = ctx.computed.revComputed.revKj;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
const revBt = ctx.computed.revComputed.revBoq.totals;
|
||||
const fullBoq = evaluateFullBoqRows(ctx);
|
||||
const rts = ctx.state.kanjubConfig.rates;
|
||||
const [bm, bl, bs] = [
|
||||
(ctx.uiState.bidRateM || 87.745) / 100,
|
||||
(ctx.uiState.bidRateL || 87.745) / 100,
|
||||
(ctx.uiState.bidRateS || 87.745) / 100,
|
||||
];
|
||||
const revByRec = Object.fromEntries(
|
||||
(ctx.state.boqItems || []).map((b) => [b._recno, b.changedQty ?? b.qty]),
|
||||
);
|
||||
return {
|
||||
source_path: ctx.currentSourcePath,
|
||||
target_folder_name: (ctx.state.projectMeta.pname || "stmate_edited").slice(0, 28),
|
||||
costn_updates: (ctx.dbfData.costn_rows || []).map((r) => ({
|
||||
CODE: r.CODE,
|
||||
MAMT1: r.MAMT1,
|
||||
LAMT1: r.LAMT1,
|
||||
SAMT1: r.SAMT1,
|
||||
TAMT1: r.TAMT1,
|
||||
AMT_61: r.TAMT1,
|
||||
})),
|
||||
costn_updates: (ctx.dbfData.costn_rows || []).map((r) => {
|
||||
const m1 = num(r.MAMT1),
|
||||
l1 = num(r.LAMT1),
|
||||
s1 = num(r.SAMT1),
|
||||
t1 = m1 + l1 + s1 || num(r.TAMT1);
|
||||
const m3 = Math.floor(m1 * bm),
|
||||
l3 = Math.floor(l1 * bl),
|
||||
s3 = Math.floor(s1 * bs);
|
||||
return {
|
||||
CODE: r.CODE,
|
||||
MAMT1: m1,
|
||||
LAMT1: l1,
|
||||
SAMT1: s1,
|
||||
TAMT1: t1,
|
||||
MAMT2: m1,
|
||||
LAMT2: l1,
|
||||
SAMT2: s1,
|
||||
TAMT2: t1,
|
||||
MAMT3: m3,
|
||||
LAMT3: l3,
|
||||
SAMT3: s3,
|
||||
TAMT3: m3 + l3 + s3,
|
||||
AMT_61: t1,
|
||||
};
|
||||
}),
|
||||
boq_updates: fullBoq.map((r) => ({
|
||||
_recno: r._recno,
|
||||
ITNUM: r.ITNUM || "",
|
||||
@@ -631,10 +599,22 @@
|
||||
SIZE1: r.SIZE1 || "",
|
||||
UNIT1: r.UNIT1 || "",
|
||||
QTY1: r.QTY1,
|
||||
QTY2: revByRec[r._recno] !== undefined ? revByRec[r._recno] : r.QTY1,
|
||||
MAMT1: r.MAMT1,
|
||||
LAMT1: r.LAMT1,
|
||||
SAMT1: r.SAMT1,
|
||||
TAMT1: r.TAMT1,
|
||||
MAMT2: r.MAMT1,
|
||||
LAMT2: r.LAMT1,
|
||||
SAMT2: r.SAMT1,
|
||||
TAMT2: r.TAMT1,
|
||||
MAMT3: Math.floor(num(r.MAMT1) * bm),
|
||||
LAMT3: Math.floor(num(r.LAMT1) * bl),
|
||||
SAMT3: Math.floor(num(r.SAMT1) * bs),
|
||||
TAMT3:
|
||||
Math.floor(num(r.MAMT1) * bm) +
|
||||
Math.floor(num(r.LAMT1) * bl) +
|
||||
Math.floor(num(r.SAMT1) * bs),
|
||||
})),
|
||||
prjlst_updates: {
|
||||
PNAME: ctx.state.projectMeta.pname,
|
||||
@@ -644,6 +624,13 @@
|
||||
SCOST1: bt.directS,
|
||||
TCOST1: bt.directT,
|
||||
COSTW1: kj.GRAND_TOTAL,
|
||||
MCOST2: revBt.directM,
|
||||
LCOST2: revBt.directL,
|
||||
SCOST2: revBt.directS,
|
||||
TCOST2: revBt.directT,
|
||||
COSTW2: revKj.GRAND_TOTAL,
|
||||
TCOST3: Math.floor(bt.directT * bm),
|
||||
COSTW3: Math.floor(kj.GRAND_TOTAL * bm),
|
||||
},
|
||||
smt_updates: {
|
||||
"1_R_KANNO": rts.KANNO,
|
||||
@@ -667,6 +654,7 @@
|
||||
"1_M_VAT": kj.VAT,
|
||||
"1_M_TOT": kj.FINAL_SUPPLY,
|
||||
"1_M_KWNGP": kj.GOV_MAT,
|
||||
"2_M_TOT": revKj.FINAL_SUPPLY,
|
||||
},
|
||||
costn_memo_updates: ctx.uiState.editedSmtMap || {},
|
||||
};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,154 @@
|
||||
[START]
|
||||
ARENM=1
|
||||
BMKNM=TLMS
|
||||
ACDNM=1
|
||||
KCDNM=1
|
||||
ZCDNM=2
|
||||
CHGNM=1
|
||||
CHNNM=1
|
||||
BCDNM=65
|
||||
DCDNM=121
|
||||
ICDNM=0
|
||||
LCDNM=20
|
||||
MCDNM=116
|
||||
PCDNM=13
|
||||
QCDNM=0
|
||||
SCDNM=35
|
||||
WCDNM=0
|
||||
XCDNM=30
|
||||
SUBNM=남부지방산림청
|
||||
LMGNM=20
|
||||
MKRNM=4
|
||||
TMGNM=10
|
||||
JEMNM=123456
|
||||
JMOK1=조달가격
|
||||
JMOK2=물가자료
|
||||
JMOK3=물가정보
|
||||
JMOK4=거래가격
|
||||
JMOK5=견적단가
|
||||
JMOK6=적용 단가
|
||||
JMK10=조달
|
||||
JMK20=물자
|
||||
JMK30=물정
|
||||
JMK40=거래
|
||||
JMK50=견적
|
||||
JUKNM=6
|
||||
YCDNM=0
|
||||
JCDNM=0
|
||||
ECDNM=0
|
||||
BPNNM=1
|
||||
OPTNM=SML
|
||||
CHBNM=1
|
||||
S_JDA=1
|
||||
USRPG=1
|
||||
CUTNM=0
|
||||
|
||||
[PRINT]
|
||||
ORDNM=
|
||||
FONT=굴림체
|
||||
F_COL0=clBlack
|
||||
F_COL1=clBlack
|
||||
F_COL2=clRed
|
||||
SH_TYP0=0
|
||||
SAN_TYP=1
|
||||
BMK_STR1=재 료 비
|
||||
BMK_STR2=노 무 비
|
||||
BMK_STR3=경 비
|
||||
BMK_STR4=합 계
|
||||
EQ_TYP=1
|
||||
SH_TYP1=1
|
||||
SH_TYP2=1
|
||||
SH_TYP3=2
|
||||
FLT_JAB=2
|
||||
CAP_CODE=코드번호
|
||||
CAP_NO=호표
|
||||
CAP_IBRD=분류번호
|
||||
CAP_EQCOD=시설코드
|
||||
CAP_ITNUM=공 종
|
||||
CAP_NAME=명 칭
|
||||
CAP_SIZE=규 격
|
||||
CAP_QTY=수 량
|
||||
CAP_SUNIT=단위
|
||||
CAP_AMT=단 가
|
||||
CAP_PAGE=페이지
|
||||
CAP_AMOUNT=금 액
|
||||
CAP_BIGO=비 고
|
||||
FMT_LEFT=-
|
||||
FMT_RIGHT=-
|
||||
RPOS_PAGE=0
|
||||
RPOS_COP=3
|
||||
RPOS_QTY=2
|
||||
RPOS_SIZE=0
|
||||
RPOS_BIGO=1
|
||||
LNP_CNT=20
|
||||
P_BIGO=1
|
||||
TITL_1LN=0
|
||||
P_SHAMT=0
|
||||
DISP_N1=소계
|
||||
DISP_N2=계
|
||||
DISP_N3=합계
|
||||
DISP_N4=총계
|
||||
DISP_N5=총합계
|
||||
DISP_N6=총대계
|
||||
DISP_EX=제외금액
|
||||
DISP_TOT=총 계
|
||||
DISP_N7=광역계
|
||||
DISP_N8=광역총계
|
||||
DISP_N9=광역대계
|
||||
CH_PRN_COD=0
|
||||
COD_FMT_0=0
|
||||
COD_FMT_1=노무
|
||||
COD_FMT_2=자재
|
||||
COD_FMT_3=경비
|
||||
COD_FMT_4=대가
|
||||
COD_FMT_5=중기
|
||||
COD_FMT_6=산근
|
||||
COD_FMT_7=기타
|
||||
COD_FMT_8=실적
|
||||
EDTNM=
|
||||
CH_PRN_ORD=0
|
||||
CH_MIN_COD=5
|
||||
UPDNCAP=상단:당초 , 하단:변경
|
||||
KAN_TYP=0
|
||||
|
||||
[EQUIPMENT]
|
||||
RXXNM=1/8*16/12*25/20
|
||||
RXTNM=
|
||||
EXLNM1=1289
|
||||
EXHNM1=1289
|
||||
EXLNM2=844.7
|
||||
EXHNM2=844.7
|
||||
EXLNM3=1187.8
|
||||
EXHNM3=1187.8
|
||||
EXLNM4=1187.8
|
||||
EXHNM4=1187.8
|
||||
CONLD=L00008L00009L00010
|
||||
CONTD=
|
||||
CON1D=
|
||||
CON2D=
|
||||
CON3D=
|
||||
CON4D=
|
||||
EXRNM=0
|
||||
RX1NM=
|
||||
RX2NM=
|
||||
RX3NM=
|
||||
RX4NM=
|
||||
EXEUR1=1000
|
||||
EXYEN1=1000
|
||||
EXEUR2=1000
|
||||
EXYEN2=1000
|
||||
|
||||
[PROJECT]
|
||||
PCODE=XXXX
|
||||
PMODE=1
|
||||
PNAME=2024년도 산불진화임도 실시설계(2개소)-24년 3월 봉�
|
||||
P_DIR=
|
||||
RGrpTo=0
|
||||
CBoxUpIns=0
|
||||
PRG_V=24.04
|
||||
|
||||
[NAG_HAL]
|
||||
ST_MHAL=0
|
||||
EQ_LHAL=0
|
||||
EQ_MHAL=0
|
||||
EQ_SHAL=0
|
||||
@@ -0,0 +1,154 @@
|
||||
[START]
|
||||
ARENM=1
|
||||
BMKNM=TLMS
|
||||
ACDNM=1
|
||||
KCDNM=1
|
||||
ZCDNM=2
|
||||
CHGNM=1
|
||||
CHNNM=1
|
||||
BCDNM=65
|
||||
DCDNM=121
|
||||
ICDNM=0
|
||||
LCDNM=20
|
||||
MCDNM=116
|
||||
PCDNM=13
|
||||
QCDNM=0
|
||||
SCDNM=35
|
||||
WCDNM=0
|
||||
XCDNM=30
|
||||
SUBNM=남부지방산림청
|
||||
LMGNM=20
|
||||
MKRNM=4
|
||||
TMGNM=10
|
||||
JEMNM=123456
|
||||
JMOK1=조달가격
|
||||
JMOK2=물가자료
|
||||
JMOK3=물가정보
|
||||
JMOK4=거래가격
|
||||
JMOK5=견적단가
|
||||
JMOK6=적용 단가
|
||||
JMK10=조달
|
||||
JMK20=물자
|
||||
JMK30=물정
|
||||
JMK40=거래
|
||||
JMK50=견적
|
||||
JUKNM=6
|
||||
YCDNM=0
|
||||
JCDNM=0
|
||||
ECDNM=0
|
||||
BPNNM=1
|
||||
OPTNM=SML
|
||||
CHBNM=1
|
||||
S_JDA=1
|
||||
USRPG=1
|
||||
CUTNM=0
|
||||
|
||||
[PRINT]
|
||||
ORDNM=
|
||||
FONT=굴림체
|
||||
F_COL0=clBlack
|
||||
F_COL1=clBlack
|
||||
F_COL2=clRed
|
||||
SH_TYP0=0
|
||||
SAN_TYP=1
|
||||
BMK_STR1=재 료 비
|
||||
BMK_STR2=노 무 비
|
||||
BMK_STR3=경 비
|
||||
BMK_STR4=합 계
|
||||
EQ_TYP=1
|
||||
SH_TYP1=1
|
||||
SH_TYP2=1
|
||||
SH_TYP3=2
|
||||
FLT_JAB=2
|
||||
CAP_CODE=코드번호
|
||||
CAP_NO=호표
|
||||
CAP_IBRD=분류번호
|
||||
CAP_EQCOD=시설코드
|
||||
CAP_ITNUM=공 종
|
||||
CAP_NAME=명 칭
|
||||
CAP_SIZE=규 격
|
||||
CAP_QTY=수 량
|
||||
CAP_SUNIT=단위
|
||||
CAP_AMT=단 가
|
||||
CAP_PAGE=페이지
|
||||
CAP_AMOUNT=금 액
|
||||
CAP_BIGO=비 고
|
||||
FMT_LEFT=-
|
||||
FMT_RIGHT=-
|
||||
RPOS_PAGE=0
|
||||
RPOS_COP=3
|
||||
RPOS_QTY=2
|
||||
RPOS_SIZE=0
|
||||
RPOS_BIGO=1
|
||||
LNP_CNT=20
|
||||
P_BIGO=1
|
||||
TITL_1LN=0
|
||||
P_SHAMT=0
|
||||
DISP_N1=소계
|
||||
DISP_N2=계
|
||||
DISP_N3=합계
|
||||
DISP_N4=총계
|
||||
DISP_N5=총합계
|
||||
DISP_N6=총대계
|
||||
DISP_EX=제외금액
|
||||
DISP_TOT=총 계
|
||||
DISP_N7=광역계
|
||||
DISP_N8=광역총계
|
||||
DISP_N9=광역대계
|
||||
CH_PRN_COD=0
|
||||
COD_FMT_0=0
|
||||
COD_FMT_1=노무
|
||||
COD_FMT_2=자재
|
||||
COD_FMT_3=경비
|
||||
COD_FMT_4=대가
|
||||
COD_FMT_5=중기
|
||||
COD_FMT_6=산근
|
||||
COD_FMT_7=기타
|
||||
COD_FMT_8=실적
|
||||
EDTNM=
|
||||
CH_PRN_ORD=0
|
||||
CH_MIN_COD=5
|
||||
UPDNCAP=상단:당초 , 하단:변경
|
||||
KAN_TYP=0
|
||||
|
||||
[EQUIPMENT]
|
||||
RXXNM=1/8*16/12*25/20
|
||||
RXTNM=
|
||||
EXLNM1=1289
|
||||
EXHNM1=1289
|
||||
EXLNM2=844.7
|
||||
EXHNM2=844.7
|
||||
EXLNM3=1187.8
|
||||
EXHNM3=1187.8
|
||||
EXLNM4=1187.8
|
||||
EXHNM4=1187.8
|
||||
CONLD=L00008L00009L00010
|
||||
CONTD=
|
||||
CON1D=
|
||||
CON2D=
|
||||
CON3D=
|
||||
CON4D=
|
||||
EXRNM=0
|
||||
RX1NM=
|
||||
RX2NM=
|
||||
RX3NM=
|
||||
RX4NM=
|
||||
EXEUR1=1000
|
||||
EXYEN1=1000
|
||||
EXEUR2=1000
|
||||
EXYEN2=1000
|
||||
|
||||
[PROJECT]
|
||||
PCODE=XXXX
|
||||
PMODE=1
|
||||
PNAME=2024년도 산불진화임도 실시설계(2개소)-24년 3월 봉�
|
||||
P_DIR=
|
||||
RGrpTo=0
|
||||
CBoxUpIns=0
|
||||
PRG_V=24.04
|
||||
|
||||
[NAG_HAL]
|
||||
ST_MHAL=0
|
||||
EQ_LHAL=0
|
||||
EQ_MHAL=0
|
||||
EQ_SHAL=0
|
||||
Binary file not shown.
Reference in New Issue
Block a user