/** * 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 { 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: "" }, 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: {}, }, }; const recompute = () => (ctx.computed = Calc.computeAll(ctx.state)); // 끼워 넣기 — scr_*.js 가 STMATE_PLUGINS 에 화면·메뉴 동작을 등록(plugins_manifest.js 가 불러옴) const PLUG = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || { screens: {}, actions: {}, menus: [], }); const API = (global.STMATE_APP = { ctx, Calc, fmt, num, $, onClick, onChange, bindEach, closeModal, }); 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}행` : "새 공사 (불러온 파일 없음)"; if (prjBar) { prjBar.innerHTML = `
열린 공사${ctx.state.projectMeta.pname}(${ctx.state.projectMeta.pnum || "-"} · 코드 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); }); } function renderWorkspace() { recompute(); renderChromeBars(); const ws = $("mdi-workspace"); if (!ws) return; const scr = PLUG.screens[ctx.uiState.activeScreen] || PLUG.screens.wPrjList; ws.innerHTML = scr ? scr.render(ctx, API) : ""; if (scr && scr.bind) scr.bind(ctx, API); } 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}내역)`; Object.assign(ctx.uiState, { boqSel: [], sysDraft: null }); if (API.noteRecent) API.noteRecent(sourcePath); 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 postSave(decorateSavePayload(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); } } // 저장 길 — 코드 관리(지움·코드 바꿈·다른 표 코드)가 있으면 /api/ui/save(구조 덧붙임 · api_ui_code.py) · 아니면 /api/save function postSave(p) { const UC = global.STMATE_UI_CODE; if (UC && UC.needsUiSave(ctx.dbfData)) return postJson( "/api/ui/save", Object.assign(p, UC.structPayload(ctx.dbfData), { rate_tables: ctx.dbfData.rate_dirty ? ctx.dbfData.rate_tables : {}, }), ); return postJson("/api/save", p); } // 저장 묶음 덧붙임 — 초기값(start_ini) · 공사목록 전체 행(prjlst_rows) · 서버 반영은 파일 담당 function decorateSavePayload(p) { const d = ctx.dbfData; if (!d) return p; // 산근 편집기 계약단가 표시(BCONT) — 고친 코드만 COSTN 칸 덧붙임 const extra = ctx.uiState.costnExtra || {}; (p.costn_updates || []).forEach((u) => Object.assign(u, extra[String(u.CODE).trim()] || {})); return Object.assign(p, { start_ini: d.start_ini || {}, prjlst_rows: d.prjlst_all || [] }); } function openModal(title, bodyHtml) { $("modal-title-text").textContent = title; $("modal-body-content").innerHTML = bodyHtml; $("stmate-modal-backdrop").classList.remove("hidden"); } // 내역 행 끼워 넣기 — app_modals.js const insertBoqRow = (...args) => global.STMATE_APP_MODALS.insertBoqRow(API, ...args); 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); } Object.assign(API, { renderWorkspace, recompute, openModal, postJson, downloadBlob }); Object.assign(API, { cascadeAndRender, loadProjectFromServer, insertBoqRow }); Object.assign(API, { saveAll: saveAllToDbfServer, decorateSavePayload, postSave, }); async function initStmateApp() { onClick("modal-close-btn", closeModal); // 원본 메뉴·툴바·단축키 — ui_app_menu.js(끼워 넣은 메뉴·동작 포함) global.STMATE_UI_APP_MENU.install(API); try { const srcRes = await fetch("/api/sources"); if (srcRes.ok) ctx.sourcesData = await srcRes.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("[실무 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);