/** * 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); 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", }, }; function 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 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 = `
열린 공사${ctx.state.projectMeta.pname}(${ctx.state.projectMeta.pnum || "2024년 단가"} · 코드 BOQ${ctx.state.projectMeta.pcode || "11"})
원본 DBF/SMT: ${dbfInfo}원가양식: ${ctx.state.kanjubConfig.scheme}형식
`; } if (stBar) { stBar.innerHTML = `
순공사원가 재료비:${fmt(bt.directM)}노무비:${fmt(bt.directL)}경비:${fmt(bt.directS)}직접공사비 계:${fmt(bt.directT)}원간접공사비+일반+이윤:${fmt(kj.TOTAL_COST - kj.DIRECT_COST)}원도급액(공급가+부가세):${fmt(kj.FINAL_SUPPLY)}원관급자재대:${fmt(kj.GOV_MAT)}원총공사비(COSTW1):${fmt(kj.GRAND_TOTAL)}원
STMATE w 8.0 Full Engine
`; } 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); } } async function saveAllToDbfServer() { if (!ctx.currentSourcePath || !ctx.dbfData) { return alert("먼저 PC 또는 실무 STMATE 프로젝트를 로드하세요."); } recompute(); const kj = ctx.computed.kanjubComputed; const bt = ctx.computed.boqComputed.totals; try { const saved = await postJson("/api/save", ViewPrjBoq.buildSavePayload(ctx)); 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)}원`, ); } 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(); } 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; 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) => { r.manualPrice = Math.floor( num(r.manualPrice) * (r.kind === "재료비" ? rm : r.kind === "노무비" ? rl : rs), ); }); 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]; 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", }); } }); } 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 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, ); }); closeModal(); renderWorkspace(); }); onClick("modal-apply-edthal", () => { const kind = $("edthal-kind").value; const pct = num($("edthal-pct").value, 100) / 100; const 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(); renderWorkspace(); }); 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 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"); }); 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()); 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(); }); 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][ 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", ].forEach((pair) => { const [id, key] = pair.split(":"); bindModalOpener(id, key); }); onClick("boq-btn-save-dbf", () => saveAllToDbfServer()); 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, }); renderWorkspace(); }); 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), ); renderWorkspace(); }); 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"); 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); if (evalRes.sumT > 0) ViewPrjBoq.syncCostnUnitPrice(ctx, code, evalRes.sumM, evalRes.sumL, evalRes.sumS); renderWorkspace(); }); 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-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; const d = await postJson("/api/load-jbr", { jbr_path: jbrPath }); 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 jbrPath = $("kr-jbr-select").value; if (!jbrPath) return; const d = await postJson("/api/load-jbr", { jbr_path: jbrPath }); 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", () => { 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); 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-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 kj = ctx.computed.kanjubComputed; downloadBlob( `\n\n ${ctx.state.projectMeta.pname}\n ${kj.DIRECT_COST}\n ${kj.FINAL_SUPPLY}\n ${kj.GRAND_TOTAL}\n`, "PPS_CostExchange.xml", "application/xml", ); }); 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"); }); } 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(":"); const html = grp === "dan" ? ViewDanIlwi.buildDanModalHtml(kind, ctx) : grp === "boq" ? ViewPrjBoq.buildBoqToolModalHtml(kind, ctx) : `

1. 실행 모듈: Start8x.exe · ST_Sub1.dll · ST_Prt.CPM

2. DBF/SMT 직결: COSTN.DBF · COSTN.SMT(SIxMemo 64B 블록 전개식, 10B <HII IFORM 포인터) · BOQ11~14.DBF · PRJLST.DBF/.SMT · RATE_R/RA/M.DBF & ST_BSC/*.JB_R

`; openModal(title, html); } 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, 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; } 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); }); 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) => ``) .join(""); sel.onchange = () => loadProjectFromServer(sel.value); const defaultPrj = 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);