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 @@
+
+