From 123727e72500ed18b6a2775e44058f9c3f94b2a6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 13 Nov 2026 18:14:42 +0900 Subject: [PATCH] =?UTF-8?q?refactor(stmate-test):=20=EB=81=BC=EC=9B=8C=20?= =?UTF-8?q?=EB=84=A3=EA=B8=B0=20=EA=B5=AC=EC=A1=B0=20=E2=80=94=20scr=5F*.j?= =?UTF-8?q?s=20=ED=99=94=EB=A9=B4=C2=B7=EB=A9=94=EB=89=B4=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EB=93=B1=EB=A1=9D=20=C2=B7=20api=5F*.py=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EC=9E=90=EB=8F=99=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=C2=B7=20app.js=20700=EC=A4=84=20=EC=95=84=EB=9E=98?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STMATE_PLUGINS(screens · actions · menus) · STMATE_APP(ctx 와 화면 도우미) — app.js 가 등록된 화면을 그리고 메뉴 단추를 붙임 - /plugins_manifest.js 가 scr_*.js 를 불러옴 · server.py 가 api_*.py 의 router 를 연결 - 도구 창 바인딩을 app_modals.js 로 분리(app.js 718 → 613줄) Co-Authored-By: Claude Opus 5.5 --- tools/stmate-test/app.js | 181 +++++++------------------------- tools/stmate-test/app_modals.js | 159 ++++++++++++++++++++++++++++ tools/stmate-test/index.html | 2 + tools/stmate-test/server.py | 12 +++ 4 files changed, 210 insertions(+), 144 deletions(-) create mode 100644 tools/stmate-test/app_modals.js diff --git a/tools/stmate-test/app.js b/tools/stmate-test/app.js index 63527aa15..57bb4d9b6 100644 --- a/tools/stmate-test/app.js +++ b/tools/stmate-test/app.js @@ -49,6 +49,24 @@ 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"); @@ -85,8 +103,12 @@ renderChromeBars(); const ws = $("mdi-workspace"); if (!ws) return; - ws.innerHTML = (RENDERERS[ctx.uiState.activeScreen] || ViewPrjBoq.renderWPrjList)(ctx); + const scr = PLUG.screens[ctx.uiState.activeScreen]; + ws.innerHTML = scr + ? scr.render(ctx, API) + : (RENDERERS[ctx.uiState.activeScreen] || ViewPrjBoq.renderWPrjList)(ctx); bindWorkspaceEvents(); + if (scr && scr.bind) scr.bind(ctx, API); } async function postJson(url, body) { @@ -154,149 +176,9 @@ 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(); - } + // 도구 창 바인딩·내역 행 끼워 넣기 — app_modals.js + const bindModalEvents = () => global.STMATE_APP_MODALS.bindModalEvents(API); + const insertBoqRow = (...args) => global.STMATE_APP_MODALS.insertBoqRow(API, ...args); function bindWorkspaceEvents() { const goScreen = (scr) => { @@ -650,6 +532,9 @@ onClick(id, () => openMappedModal(key)); } + Object.assign(API, { renderWorkspace, recompute, openModal, postJson, downloadBlob }); + Object.assign(API, { cascadeAndRender, loadProjectFromServer, insertBoqRow }); + async function initStmateApp() { bindEach("#main-toolbar .tb-btn[data-screen]", "onclick", (btn) => { ctx.uiState.activeScreen = btn.getAttribute("data-screen"); @@ -657,8 +542,16 @@ }); onClick("btn-calc-all", saveAllToDbfServer); onClick("modal-close-btn", closeModal); + // 끼워 넣은 메뉴 단추 — m.menu = 상단 메뉴 data-menu(m1~m5) + PLUG.menus.forEach((m) => { + const head = document.querySelector(`#main-menubar [data-menu="${m.menu}"]`); + const box = head && head.querySelector(".menu-dropdown"); + if (box && !box.querySelector(`[data-act="${m.act}"]`)) + box.insertAdjacentHTML("beforeend", ``); + }); bindEach("#main-menubar [data-act]", "onclick", (btn) => { const act = btn.getAttribute("data-act"); + if (PLUG.actions[act]) return PLUG.actions[act](ctx, API, btn); 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) diff --git a/tools/stmate-test/app_modals.js b/tools/stmate-test/app_modals.js new file mode 100644 index 000000000..b74a5e149 --- /dev/null +++ b/tools/stmate-test/app_modals.js @@ -0,0 +1,159 @@ +/** + * STMATE 도구 창(wITNum · wBoqRate · wM_Rate · wExpnd · wQtyList · wM_Boxa · wM_Edt_Hal) 바인딩 (app_modals.js) + * app.js 에서 떼어 냄 — api = window.STMATE_APP + */ +(function (global) { + "use strict"; + + const ViewPrjBoq = () => global.STMATE_VIEW_PRJ_BOQ; + + function bindModalEvents(api) { + const { ctx, Calc, num, $, onClick, bindEach, closeModal, renderWorkspace, cascadeAndRender } = + api; + const insertBoqRow = (...a) => api.insertBoqRow(...a); + 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(api, code, name, spec, unit, qty) { + const { ctx, renderWorkspace } = api; + 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(); + } + + global.STMATE_APP_MODALS = { bindModalEvents, insertBoqRow }; +})(window); diff --git a/tools/stmate-test/index.html b/tools/stmate-test/index.html index fc1ac8815..9c38caef5 100644 --- a/tools/stmate-test/index.html +++ b/tools/stmate-test/index.html @@ -177,6 +177,8 @@ + + diff --git a/tools/stmate-test/server.py b/tools/stmate-test/server.py index 2a7406229..90fc135d8 100644 --- a/tools/stmate-test/server.py +++ b/tools/stmate-test/server.py @@ -3,6 +3,7 @@ from __future__ import annotations +import importlib import pathlib import re import zipfile @@ -645,6 +646,17 @@ def api_export_xlsx(req: ExportXlsxReq): raise HTTPException(status_code=500, detail=str(e)) +# 끼워 넣기 — api_*.py 의 router 자동 연결 · scr_*.js 화면 파일 자동 불러오기 +for _mod in sorted(HERE.glob("api_*.py")): + app.include_router(importlib.import_module(_mod.stem).router) + + +@app.get("/plugins_manifest.js") +def plugins_manifest(): + tags = "".join(f'' for f in sorted(HERE.glob("scr_*.js"))) + return Response(content=f"document.write({tags!r});", media_type="application/javascript") + + app.mount("/", StaticFiles(directory=str(HERE), html=True), name="static") if __name__ == "__main__":