refactor(stmate-test): 끼워 넣기 구조 — scr_*.js 화면·메뉴 자동 등록 · api_*.py 라우터 자동 연결 · app.js 700줄 아래로
- 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 <noreply@anthropic.com>
This commit is contained in:
+37
-144
@@ -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", `<button data-act="${m.act}">${m.label}</button>`);
|
||||
});
|
||||
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)
|
||||
|
||||
@@ -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);
|
||||
@@ -177,6 +177,8 @@
|
||||
<script src="view_prj_boq.js"></script>
|
||||
<script src="view_dan_ilwi.js"></script>
|
||||
<script src="view_kanjub_prt.js"></script>
|
||||
<script src="app_modals.js"></script>
|
||||
<script src="plugins_manifest.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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'<script src="{f.name}"></script>' 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__":
|
||||
|
||||
Reference in New Issue
Block a user