719 lines
28 KiB
JavaScript
719 lines
28 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 메인 컨트롤러 및 DBF/SMT/STC/JB_R 양방향 브리지 (app.js)
|
|
* TWM_STMATE_START_8X 5대 메뉴 · 9개 메인 화면 · 계산완료(Act_Calc_All) · 모달 제어
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
const Calc = global.STMATE_CALC;
|
|
const ViewPrjBoq = global.STMATE_VIEW_PRJ_BOQ;
|
|
const ViewDanIlwi = global.STMATE_VIEW_DAN_ILWI;
|
|
const ViewKanjubPrt = global.STMATE_VIEW_KANJUB_PRT;
|
|
const { fmt, num } = Calc;
|
|
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(),
|
|
computed: null,
|
|
sourcesData: { projects: [], jbr_files: [], work_root: "" },
|
|
catalogData: { codes: [], recipes: [], special_san: [] },
|
|
currentSourcePath: "",
|
|
dbfData: null,
|
|
uiState: {
|
|
activeScreen: "wPrjList",
|
|
showBoqPreview: true,
|
|
boqLevelFilter: "ALL",
|
|
boqSearch: "",
|
|
activePocket: 1,
|
|
boqPockets: { 1: [], 2: [], 3: [] },
|
|
danFilter: "ALL",
|
|
danSearch: "",
|
|
ilwiSubFilter: "ALL_BDX",
|
|
selectedIlwiCode: "",
|
|
editedSmtMap: {},
|
|
kanRateTab: "RATE_R.DBF",
|
|
stageSubTab: "CONTRACT",
|
|
bidRateM: 87.745,
|
|
bidRateL: 87.745,
|
|
bidRateS: 87.745,
|
|
prevPct: 30.0,
|
|
progPct: 65.0,
|
|
activeReportSheet: "0608",
|
|
},
|
|
};
|
|
|
|
const recompute = () => (ctx.computed = Calc.computeAll(ctx.state));
|
|
|
|
function renderChromeBars() {
|
|
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>`;
|
|
}
|
|
if (stBar) {
|
|
stBar.innerHTML = `<div class="st-metrics"><span class="st-kv">순공사원가 재료비:<strong>${fmt(bt.directM)}</strong></span><span class="st-kv">노무비:<strong>${fmt(bt.directL)}</strong></span><span class="st-kv">경비:<strong>${fmt(bt.directS)}</strong></span><span class="st-kv">직접공사비 계:<strong>${fmt(bt.directT)}원</strong></span><span class="st-kv">간접공사비+일반+이윤:<strong>${fmt(kj.TOTAL_COST - kj.DIRECT_COST)}원</strong></span><span class="st-kv">도급액(공급가+부가세):<strong>${fmt(kj.FINAL_SUPPLY)}원</strong></span><span class="st-kv">관급자재대:<strong>${fmt(kj.GOV_MAT)}원</strong></span><span class="st-kv total">총공사비(COSTW1):<strong>${fmt(kj.GRAND_TOTAL)}원</strong></span></div><div>STMATE w 8.0 Full Engine</div>`;
|
|
}
|
|
document.querySelectorAll("#main-toolbar .tb-btn[data-screen]").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.getAttribute("data-screen") === ctx.uiState.activeScreen);
|
|
});
|
|
}
|
|
|
|
const RENDERERS = {
|
|
wPrjList: ViewPrjBoq.renderWPrjList,
|
|
wBoq: ViewPrjBoq.renderWBoq,
|
|
wM_Set_Sys: ViewDanIlwi.renderWMSetSys,
|
|
wM_JabPum: ViewDanIlwi.renderWMJabPum,
|
|
wM_Edit_iLWi: ViewDanIlwi.renderWMEditIlwi,
|
|
wM_KanJub: ViewKanjubPrt.renderWMKanJub,
|
|
wM_KAN_RATE: ViewKanjubPrt.renderWMKanRate,
|
|
wStage: ViewKanjubPrt.renderWStage,
|
|
PM_Prt: ViewKanjubPrt.renderPMPrt,
|
|
};
|
|
|
|
function renderWorkspace() {
|
|
recompute();
|
|
renderChromeBars();
|
|
const ws = $("mdi-workspace");
|
|
if (!ws) return;
|
|
ws.innerHTML = (RENDERERS[ctx.uiState.activeScreen] || ViewPrjBoq.renderWPrjList)(ctx);
|
|
bindWorkspaceEvents();
|
|
}
|
|
|
|
async function postJson(url, body) {
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) throw new Error(await res.text());
|
|
return res.json();
|
|
}
|
|
|
|
async function loadProjectFromServer(sourcePath, jumpScreen) {
|
|
const badge = $("conn-badge");
|
|
try {
|
|
if (badge) badge.textContent = "DBF/SMT 로딩중...";
|
|
const data = await postJson("/api/load", { source_path: sourcePath });
|
|
ctx.currentSourcePath = sourcePath;
|
|
ctx.dbfData = data;
|
|
ctx.state = data.sim_state;
|
|
ctx.uiState.editedSmtMap = {};
|
|
if (jumpScreen) ctx.uiState.activeScreen = jumpScreen;
|
|
const sel = $("top-project-select");
|
|
if (sel) sel.value = sourcePath;
|
|
if (badge) badge.textContent = `DBF/SMT 직결 (${data.costn_count}단가·${data.boq_count}내역)`;
|
|
renderWorkspace();
|
|
} catch (err) {
|
|
if (badge) badge.textContent = "로드 오류";
|
|
alert("프로젝트 DBF 로드 실패: " + err.message);
|
|
}
|
|
}
|
|
|
|
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,
|
|
bt = ctx.computed.boqComputed.totals;
|
|
try {
|
|
const saved = await postJson("/api/save", ViewPrjBoq.buildSavePayload(ctx));
|
|
renderWorkspace();
|
|
alert(
|
|
`[계산완료(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);
|
|
}
|
|
}
|
|
|
|
function openModal(title, bodyHtml) {
|
|
$("modal-title-text").textContent = title;
|
|
$("modal-body-content").innerHTML = bodyHtml;
|
|
$("stmate-modal-backdrop").classList.remove("hidden");
|
|
bindModalEvents();
|
|
}
|
|
|
|
function bindModalEvents() {
|
|
onClick("modal-apply-itnum", () => {
|
|
if (!ctx.dbfData || !ctx.dbfData.boq_rows) return;
|
|
let t4 = 0,
|
|
t2 = 0,
|
|
lf = 0;
|
|
const fmtVal = $("itnum-leaf-fmt").value;
|
|
ctx.dbfData.boq_rows.forEach((r) => {
|
|
const bd = String(r.BDCODE1 || "").trim();
|
|
if (bd === "T4") ((t4 += 1), (t2 = 0), (r.ITNUM = `${t4}.`));
|
|
else if (bd === "T2") ((t2 += 1), (lf = 0), (r.ITNUM = `${t4}.${t2}`));
|
|
else if (bd && !/^(T\d|E\d|C\d|:)$/.test(bd)) {
|
|
lf += 1;
|
|
r.ITNUM = fmtVal === "01" ? String(lf).padStart(2, "0") : fmtVal === "-1" ? `-${lf}` : "";
|
|
}
|
|
});
|
|
closeModal();
|
|
renderWorkspace();
|
|
});
|
|
onClick("modal-apply-boqrate", () => {
|
|
const [rm, rl, rs] = ["m", "l", "s"].map((k) => num($(`boqrate-${k}`).value, 100) / 100);
|
|
ctx.state.resources.forEach((r) => {
|
|
const mult = r.kind === "재료비" ? rm : r.kind === "노무비" ? rl : rs;
|
|
r.manualPrice = Math.floor(num(r.manualPrice) * mult);
|
|
});
|
|
closeModal();
|
|
renderWorkspace();
|
|
});
|
|
onClick("modal-apply-mrate", () => {
|
|
ctx.state.sysConfig.ecdDigits = parseInt($("mrate-digits").value, 10) || 0;
|
|
closeModal();
|
|
renderWorkspace();
|
|
});
|
|
onClick("modal-apply-expnd", () => {
|
|
const code = ($("expnd-code") || {}).value;
|
|
const ilwi =
|
|
(ctx.state.ilwiItems || []).find((x) => x.code === code) || ctx.state.ilwiItems[0];
|
|
((ilwi && ilwi.lines) || []).forEach((ln) => {
|
|
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();
|
|
});
|
|
bindEach(".btn-apply-qty-expr", "onclick", (btn) => {
|
|
const id = btn.getAttribute("data-id");
|
|
const inp = document.querySelector(`.qty-expr-input[data-id="${id}"]`);
|
|
const val = inp ? Calc.evalFormula(inp.value, {}) : 0;
|
|
const item = ctx.state.boqItems.find((b) => b.id === id);
|
|
if (item && val > 0) {
|
|
item.qty = Math.round(val * 100) / 100;
|
|
const drow =
|
|
ctx.dbfData &&
|
|
ctx.dbfData.boq_rows &&
|
|
ctx.dbfData.boq_rows.find((x) => x._recno === item._recno);
|
|
if (drow) drow.QTY1 = String(item.qty);
|
|
renderWorkspace();
|
|
}
|
|
});
|
|
onClick("modal-apply-boxa", () => {
|
|
const mode = $("boxa-adopt-mode").value;
|
|
(ctx.dbfData ? ctx.dbfData.costn_rows : []).forEach((r) => {
|
|
const code = String(r.CODE || "").trim();
|
|
if (!code.startsWith("M")) return;
|
|
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();
|
|
cascadeAndRender();
|
|
});
|
|
onClick("modal-apply-edthal", () => {
|
|
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;
|
|
const fn = (v) => (rmode === "round" ? Math.round(v * pct) : Math.floor(v * pct));
|
|
ViewPrjBoq.syncCostnUnitPrice(
|
|
ctx,
|
|
code,
|
|
fn(num(r.MAMT1)),
|
|
fn(num(r.LAMT1)),
|
|
fn(num(r.SAMT1)),
|
|
);
|
|
});
|
|
closeModal();
|
|
cascadeAndRender();
|
|
});
|
|
bindEach(".btn-use-special-san", "onclick", (btn) => {
|
|
const spec = (ctx.catalogData.special_san || [])[parseInt(btn.getAttribute("data-idx"), 10)];
|
|
if (!spec) return;
|
|
ctx.uiState.editedSmtMap[ctx.uiState.selectedIlwiCode || "D00016"] = (
|
|
spec["산식행"] || []
|
|
).join("\r\n");
|
|
ctx.uiState.activeScreen = "wM_Edit_iLWi";
|
|
closeModal();
|
|
renderWorkspace();
|
|
});
|
|
}
|
|
|
|
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;
|
|
renderWorkspace();
|
|
};
|
|
const findBoqDrow = (recno) =>
|
|
ctx.dbfData && ctx.dbfData.boq_rows && ctx.dbfData.boq_rows.find((r) => r._recno === recno);
|
|
|
|
bindEach(".prj-row", "ondblclick", (tr) => {
|
|
const p = tr.getAttribute("data-path");
|
|
p === ctx.currentSourcePath ? goScreen("wBoq") : loadProjectFromServer(p, "wBoq");
|
|
});
|
|
bindEach(".btn-load-prj", "onclick", (btn, e) => {
|
|
e.stopPropagation();
|
|
const p = btn.getAttribute("data-path");
|
|
p === ctx.currentSourcePath ? goScreen("wBoq") : loadProjectFromServer(p, "wPrjList");
|
|
});
|
|
"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();
|
|
});
|
|
onClick("prj-btn-edit-meta", () => {
|
|
const nextName = prompt("공사명(PNAME) 수정:", ctx.state.projectMeta.pname);
|
|
if (nextName) {
|
|
ctx.state.projectMeta.pname = nextName.trim();
|
|
if (ctx.dbfData && ctx.dbfData.prjlst) ctx.dbfData.prjlst.PNAME = nextName.trim();
|
|
renderWorkspace();
|
|
}
|
|
});
|
|
|
|
onChange("boq-level-filter", (e) => {
|
|
ctx.uiState.boqLevelFilter = e.target.value;
|
|
renderWorkspace();
|
|
});
|
|
const boqSearchInp = $("boq-search-input");
|
|
if (boqSearchInp)
|
|
boqSearchInp.oninput = () => {
|
|
ctx.uiState.boqSearch = boqSearchInp.value;
|
|
renderWorkspace();
|
|
};
|
|
|
|
bindEach(".boq-edit-qty, .boq-edit-name, .boq-edit-itnum", "onchange", (inp) => {
|
|
const recno = parseInt(inp.getAttribute("data-recno"), 10);
|
|
const drow = findBoqDrow(recno);
|
|
const leaf = ctx.state.boqItems.find(
|
|
(b) => b._recno === recno || b.code === inp.getAttribute("data-code"),
|
|
);
|
|
if (inp.classList.contains("boq-edit-qty")) {
|
|
const v = num(inp.value, 0);
|
|
if (leaf) leaf.qty = v;
|
|
if (drow) drow.QTY1 = String(v);
|
|
renderWorkspace();
|
|
} else if (inp.classList.contains("boq-edit-name")) {
|
|
if (drow) drow.NAME1 = inp.value;
|
|
if (leaf) leaf.name = inp.value;
|
|
} else if (drow) {
|
|
drow.ITNUM = inp.value;
|
|
}
|
|
});
|
|
bindEach(".boq-jump-code", "onclick", (a, e) => {
|
|
e.preventDefault();
|
|
const code = a.getAttribute("data-code");
|
|
ctx.uiState.selectedIlwiCode = code;
|
|
goScreen(/^[MLS]/.test(code) ? "wM_JabPum" : "wM_Edit_iLWi");
|
|
});
|
|
bindEach(".boq-put-pocket", "onclick", (btn) => {
|
|
ctx.uiState.boqPockets[ctx.uiState.activePocket || 1].push({
|
|
code: btn.getAttribute("data-code"),
|
|
name: btn.getAttribute("data-name"),
|
|
qty: num(btn.getAttribute("data-qty"), 1),
|
|
});
|
|
renderWorkspace();
|
|
});
|
|
bindEach(".pocket-tab-btn", "onclick", (btn) => {
|
|
ctx.uiState.activePocket = parseInt(btn.getAttribute("data-pocket"), 10);
|
|
renderWorkspace();
|
|
});
|
|
bindEach(".pocket-insert-btn", "onclick", (btn) => {
|
|
const item =
|
|
ctx.uiState.boqPockets[ctx.uiState.activePocket || 1][
|
|
parseInt(btn.getAttribute("data-idx"), 10)
|
|
];
|
|
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,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",
|
|
rawName: "신규 공종",
|
|
spec: "",
|
|
unit: "m3",
|
|
};
|
|
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),
|
|
usdRate2: num($("sys-usd2").value, 1400),
|
|
useChangedUsd: $("sys-usd-mode").value === "1",
|
|
ecdDigits: parseInt($("sys-ecd").value, 10) || 0,
|
|
chnMode: $("sys-chn").value,
|
|
optOrder: $("sys-opt").value,
|
|
rxFormula: $("sys-rx-formula").value,
|
|
condCodes: $("sys-cond-codes").value,
|
|
});
|
|
cascadeAndRender();
|
|
});
|
|
bindEach(".dan-filter-btn", "onclick", (btn) => {
|
|
ctx.uiState.danFilter = btn.getAttribute("data-filter");
|
|
renderWorkspace();
|
|
});
|
|
const danSearch = $("dan-search-input");
|
|
if (danSearch)
|
|
danSearch.oninput = () => {
|
|
ctx.uiState.danSearch = danSearch.value;
|
|
renderWorkspace();
|
|
};
|
|
bindEach(".dan-edit-m, .dan-edit-l, .dan-edit-s", "onchange", (inp) => {
|
|
const tr = inp.closest("tr");
|
|
ViewPrjBoq.syncCostnUnitPrice(
|
|
ctx,
|
|
inp.getAttribute("data-code"),
|
|
num(tr.querySelector(".dan-edit-m").value),
|
|
num(tr.querySelector(".dan-edit-l").value),
|
|
num(tr.querySelector(".dan-edit-s").value),
|
|
);
|
|
cascadeAndRender();
|
|
});
|
|
bindEach(".dan-jump-ilwi", "onclick", (btn) => {
|
|
ctx.uiState.selectedIlwiCode = btn.getAttribute("data-code");
|
|
goScreen("wM_Edit_iLWi");
|
|
});
|
|
|
|
bindEach(".ilwi-sub-btn", "onclick", (btn) => {
|
|
ctx.uiState.ilwiSubFilter = btn.getAttribute("data-sub");
|
|
renderWorkspace();
|
|
});
|
|
bindEach(".ilwi-master-row", "onclick", (tr) => {
|
|
ctx.uiState.selectedIlwiCode = tr.getAttribute("data-code");
|
|
renderWorkspace();
|
|
});
|
|
const smtArea = $("smt-formula-textarea");
|
|
if (smtArea)
|
|
smtArea.oninput = () =>
|
|
(ctx.uiState.editedSmtMap[smtArea.getAttribute("data-code")] = smtArea.value);
|
|
onClick("ilwi-btn-apply-f9", () => {
|
|
const code = $("ilwi-btn-apply-f9").getAttribute("data-code");
|
|
const txt = ($("smt-formula-textarea") || {}).value || "";
|
|
ctx.uiState.editedSmtMap[code] = txt;
|
|
if (ctx.dbfData && ctx.dbfData.costn_memos) {
|
|
const prev = ctx.dbfData.costn_memos[code] || { code, blockNo: 1 };
|
|
ctx.dbfData.costn_memos[code] = Object.assign({}, prev, { text: txt, byteLen: txt.length });
|
|
}
|
|
const evalRes = ViewDanIlwi.evaluateSmtFormulaText(txt, ctx, code);
|
|
if (evalRes.sumT > 0)
|
|
ViewPrjBoq.syncCostnUnitPrice(ctx, code, evalRes.sumM, evalRes.sumL, evalRes.sumS);
|
|
cascadeAndRender();
|
|
});
|
|
|
|
bindEach(".kj-scheme-tab", "onclick", (btn) => {
|
|
ctx.state.kanjubConfig.scheme = btn.getAttribute("data-scheme");
|
|
renderWorkspace();
|
|
});
|
|
bindEach(".kj-rate-input", "onchange", (inp) => {
|
|
ctx.state.kanjubConfig.rates[inp.getAttribute("data-key")] = num(inp.value);
|
|
renderWorkspace();
|
|
});
|
|
[
|
|
["kj-opt-anjun-gov", "anjunGovVatExcluded", (v) => v === "1"],
|
|
["kj-anjun-base", "anjunBaseAdd", (v) => num(v)],
|
|
["kj-anjun-cap", "anjunCap12", (v) => v === "1"],
|
|
["kj-ind-trunc", "indirectTruncDigits", (v) => parseInt(v, 10)],
|
|
["kj-vat-mode", "vatMode", (v) => v],
|
|
["kj-tot-jeol", "totJeolDigits", (v) => parseInt(v, 10)],
|
|
["kj-profit-adj", "enableProfitEndAdj", (v) => v === "1"],
|
|
["kj-gov-round", "govRoundMode", (v) => v],
|
|
].forEach(([id, key, fn]) =>
|
|
onChange(id, (e) => {
|
|
ctx.state.kanjubConfig[key] = fn(e.target.value);
|
|
renderWorkspace();
|
|
}),
|
|
);
|
|
onClick("kj-btn-apply-jbr", async () => {
|
|
const p = $("kj-jbr-select").value;
|
|
if (!p) return;
|
|
const d = await postJson("/api/load-jbr", { jbr_path: p });
|
|
Object.assign(ctx.state.kanjubConfig.rates, d.extracted_rates);
|
|
if (d.extracted_rates.anjunBaseAdd)
|
|
ctx.state.kanjubConfig.anjunBaseAdd = d.extracted_rates.anjunBaseAdd;
|
|
renderWorkspace();
|
|
});
|
|
bindEach(".kr-tab-btn", "onclick", (btn) => {
|
|
ctx.uiState.kanRateTab = btn.getAttribute("data-tab");
|
|
renderWorkspace();
|
|
});
|
|
onClick("kr-btn-load-jbr", async () => {
|
|
const p = $("kr-jbr-select").value;
|
|
if (!p) return;
|
|
const d = await postJson("/api/load-jbr", { jbr_path: p });
|
|
if (ctx.dbfData && ctx.dbfData.rate_tables) {
|
|
ctx.dbfData.rate_tables["RATE_R.DBF"] = d.rows;
|
|
ctx.uiState.kanRateTab = "RATE_R.DBF";
|
|
renderWorkspace();
|
|
}
|
|
});
|
|
bindEach(".stage-tab-btn", "onclick", (btn) => {
|
|
ctx.uiState.stageSubTab = btn.getAttribute("data-st");
|
|
renderWorkspace();
|
|
});
|
|
onClick("st-btn-apply-bid", () => {
|
|
["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", () => {
|
|
ctx.uiState.prevPct = num($("st-prev-pct").value, 30);
|
|
ctx.uiState.progPct = num($("st-prog-pct").value, 65);
|
|
renderWorkspace();
|
|
});
|
|
bindEach(".rev-edit-qty2", "onchange", (inp) => {
|
|
const item = ctx.state.boqItems.find((b) => b.id === inp.getAttribute("data-id"));
|
|
if (item) item.changedQty = num(inp.value);
|
|
renderWorkspace();
|
|
});
|
|
|
|
onChange("prt-sheet-select", (e) => {
|
|
ctx.uiState.activeReportSheet = e.target.value;
|
|
renderWorkspace();
|
|
});
|
|
onClick("prt-btn-xls", () => exportFullXlsx());
|
|
onClick("prt-btn-csv", () => {
|
|
const tbl = document.querySelector("#mdi-workspace table.dbgrid");
|
|
if (!tbl) return;
|
|
const rows = Array.from(tbl.querySelectorAll("tr")).map((tr) =>
|
|
Array.from(tr.querySelectorAll("th,td"))
|
|
.map((td) => `"${td.innerText.replace(/"/g, '""').trim()}"`)
|
|
.join(","),
|
|
);
|
|
downloadBlob(
|
|
"\uFEFF" + rows.join("\r\n"),
|
|
`STMATE_Report_${ctx.uiState.activeReportSheet}.csv`,
|
|
"text/csv;charset=utf-8",
|
|
);
|
|
});
|
|
onClick("prt-btn-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(
|
|
(r) => `JDL|${r.code}|${r.name}|${r.spec}|${r.unit}|${r.qty}|${r.tUnit}|${r.tAmt}`,
|
|
);
|
|
downloadBlob(lines.join("\r\n"), "STMATE_Export.JDL", "text/plain;charset=utf-8");
|
|
});
|
|
}
|
|
|
|
async function exportFullXlsx() {
|
|
try {
|
|
const fullBoq = ViewPrjBoq.evaluateFullBoqRows(ctx);
|
|
const res = await fetch("/api/export-xlsx", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
prj_meta: ctx.state.projectMeta,
|
|
costn_rows: (ctx.dbfData && ctx.dbfData.costn_rows) || [],
|
|
boq_rows: fullBoq,
|
|
kanjub_data: ctx.computed.kanjubComputed,
|
|
costn_memos: (ctx.dbfData && ctx.dbfData.costn_memos) || {},
|
|
recipes: (ctx.catalogData && ctx.catalogData.recipes) || [],
|
|
}),
|
|
});
|
|
if (!res.ok) throw new Error(await res.text());
|
|
downloadBlob(
|
|
await res.blob(),
|
|
`STMATE_Workbook_${ctx.state.projectMeta.pcode || "11"}.xlsx`,
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
);
|
|
} catch (err) {
|
|
if (!global.STMATE_ENGINE) return alert("엑셀 내보내기 실패: " + err.message);
|
|
downloadBlob(
|
|
global.STMATE_ENGINE.buildMultiSheetExcelXml(ctx),
|
|
`STMATE_Workbook_${ctx.state.projectMeta.pcode || "11"}.xls`,
|
|
"application/vnd.ms-excel;charset=utf-8",
|
|
);
|
|
}
|
|
}
|
|
|
|
function downloadBlob(content, filename, mime) {
|
|
const a = document.createElement("a");
|
|
a.href = URL.createObjectURL(new Blob([content], { type: mime }));
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
|
|
const MENU_SCREEN_MAP = {
|
|
Act_PrjList: "wPrjList",
|
|
Act_Init: "wM_Set_Sys",
|
|
Act_BoqEdit: "wBoq",
|
|
Act_KanJub: "wM_KanJub",
|
|
Act_KanRate: "wM_KAN_RATE",
|
|
Act_StageEdit: "wStage",
|
|
Act_Print: "PM_Prt",
|
|
};
|
|
|
|
const MENU_MODAL_MAP = {
|
|
Act_BoxaModal: ["5개 가격지 비교 (wM_Boxa)", "dan:wM_Boxa"],
|
|
Act_EdtHalModal: ["기준단가 비율 일괄수정 (wM_Edt_Hal)", "dan:wM_Edt_Hal"],
|
|
Act_CatalogModal: ["STMATE 표준 카탈로그 브라우저", "dan:CATALOG"],
|
|
Act_ITNumModal: ["내역서 공종번호 자동부여 (wITNum)", "boq:wITNum"],
|
|
Act_BoqRateModal: ["내역 할증율 적용 (wBoqRate)", "boq:wBoqRate"],
|
|
Act_MRateModal: ["비목 단수 일괄보정 (wM_Rate)", "boq:wM_Rate"],
|
|
Act_ExpndModal: ["일위대가 풀어넣기 (wExpnd)", "boq:wExpnd"],
|
|
Act_QtyModal: ["수량산출서 연결 집계 (wQtyList)", "boq:wQtyList"],
|
|
Act_ReaInfoModal: ["STMATE w 8.0 REA 정적분석 아키텍처 & DBF/SMT 명세", "rea:info"],
|
|
};
|
|
|
|
function openMappedModal(key) {
|
|
const [title, spec] = MENU_MODAL_MAP[key];
|
|
const [grp, kind] = spec.split(":");
|
|
openModal(
|
|
title,
|
|
grp === "dan"
|
|
? ViewDanIlwi.buildDanModalHtml(kind, ctx)
|
|
: grp === "boq"
|
|
? ViewPrjBoq.buildBoqToolModalHtml(kind, ctx)
|
|
: `<div style="line-height:1.6;"><p><strong>1. 실행 모듈:</strong> <code>Start8x.exe</code> · <code>ST_Sub1.dll</code> · <code>ST_Prt.CPM</code></p><p><strong>2. DBF/SMT 직결:</strong> <code>COSTN.DBF</code> · <code>COSTN.SMT</code> · <code>BOQ11~14.DBF</code> · <code>PRJLST.DBF/.SMT</code> · <code>RATE_R/RA/M.DBF</code></p></div>`,
|
|
);
|
|
}
|
|
|
|
function bindModalOpener(id, key) {
|
|
onClick(id, () => openMappedModal(key));
|
|
}
|
|
|
|
async function initStmateApp() {
|
|
bindEach("#main-toolbar .tb-btn[data-screen]", "onclick", (btn) => {
|
|
ctx.uiState.activeScreen = btn.getAttribute("data-screen");
|
|
renderWorkspace();
|
|
});
|
|
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];
|
|
else if (MENU_MODAL_MAP[act]) return openMappedModal(act);
|
|
else if (act === "Act_ReloadDbf" && ctx.currentSourcePath)
|
|
return loadProjectFromServer(ctx.currentSourcePath);
|
|
else if (act === "Act_SaveDbf") return saveAllToDbfServer();
|
|
else if (act === "Act_ExportJson")
|
|
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 r = new FileReader();
|
|
r.onload = () => ((ctx.state = JSON.parse(String(r.result))), renderWorkspace());
|
|
r.readAsText(file);
|
|
});
|
|
|
|
try {
|
|
const [srcRes, catRes] = await Promise.all([fetch("/api/sources"), fetch("/api/catalog")]);
|
|
if (srcRes.ok) ctx.sourcesData = await srcRes.json();
|
|
if (catRes.ok) ctx.catalogData = await catRes.json();
|
|
const sel = $("top-project-select");
|
|
if (sel && ctx.sourcesData.projects.length) {
|
|
sel.innerHTML = ctx.sourcesData.projects
|
|
.map((p) => `<option value="${p.path}">${p.name}</option>`)
|
|
.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");
|
|
}
|
|
} catch (_e) {
|
|
// 오프라인 기본 폴백
|
|
}
|
|
renderWorkspace();
|
|
}
|
|
|
|
document.readyState === "loading"
|
|
? document.addEventListener("DOMContentLoaded", initStmateApp)
|
|
: initStmateApp();
|
|
})(window);
|