From 1cc9dca9fcf00fe617dc38a99b191c066ea5934a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 4 Nov 2026 22:23:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(tools):=20STMATE=20=EC=9B=90=EB=B3=B8?= =?UTF-8?q?=20DBF/SMT/STC/JB=5FR=20=EC=A7=81=EC=A0=91=20=EC=9D=BD=EA=B8=B0?= =?UTF-8?q?=C2=B7=EC=93=B0=EA=B8=B0=20=EB=B8=8C=EB=A6=AC=EC=A7=80=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EB=B0=8F=20UI=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(tools/stmate-test)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/stmate-test/README.md | 14 +- tools/stmate-test/app.js | 16 +- tools/stmate-test/db_bridge.js | 345 +++++++++++++++++ tools/stmate-test/index.html | 2 + tools/stmate-test/server.py | 687 +++++++++++++++++++++++++++++++++ 5 files changed, 1060 insertions(+), 4 deletions(-) create mode 100644 tools/stmate-test/db_bridge.js create mode 100644 tools/stmate-test/server.py diff --git a/tools/stmate-test/README.md b/tools/stmate-test/README.md index 1fc154232..1b846aab4 100644 --- a/tools/stmate-test/README.md +++ b/tools/stmate-test/README.md @@ -4,10 +4,18 @@ 경쟁사 설계예산 프로그램 **STMATE**(경영정보(주))에 대한 **REA 6.7.0 / Ghidra 12.1.4 정적 분석** 및 지식DB(`resources/knowledge/original/경쟁사 프로그램/STmate 분석/`) 결과를 바탕으로, 주요 기능과 계산 사슬·제어 옵션을 브라우저에서 독립적으로 확인하고 조작할 수 있도록 만든 테스트 웹앱입니다. -- **완전 분리 동작**: Aislo 본체 서버·DB·로그인 세션과 무관하게 `index.html` 단독으로 동작합니다. -- **상태 보관 및 입출력**: 현재 브라우저 탭(`sessionStorage`)에 자동 보관되며, 상단 버튼으로 **JSON 저장 · 파일 열기 · 예제 초기화**를 지원합니다. +- **완전 분리 동작**: Aislo 본체 서버·공용 MariaDB·로그인 세션과 무관하게 동작합니다. +- **PC 내 STMATE 원본 DBF · STC · JB_R 직접 읽기/쓰기 연동 (`server.py`)**: + - 실행 명령: `.\venv\Scripts\python.exe tools\stmate-test\server.py` (포트 `8095`, `http://127.0.0.1:8095`) + - PC의 `C:\경영정보(주)\STmate 자료\설계자료\실습(맛보기)` 원본 DBF 폴더, `C:\경영정보(주)\System\ST_BSC\*.JB_R` 제비율 기준 DB(2015~2026년 41종), 및 `resources/knowledge/original/실무문서/*.STC` 실무 프로젝트 DB를 직접 읽어 시뮬레이터에 동기화하고, 수정된 단가·수량·제비율 결과를 CP949 고정폭 바이너리 DBF(`COSTN.DBF`, `BOQ11/12.DBF`, `PRJLST.DBF/.SMT`) 및 `.STC` 패키지로 직접 기록합니다. +- **상태 보관 및 입출력**: 현재 브라우저 탭(`sessionStorage`)에 자동 보관되며, 상단 버튼으로 **JSON 저장 · 파일 열기 · 예제 초기화**도 함께 지원합니다. -## 주요 확인 및 제어 기능 (6개 탭) +## 주요 확인 및 제어 기능 (7개 탭) + +0. **00 원본 DBF · STC 연동 (`server.py`, `db_bridge.js`)** + - PC 내 STMATE 원본 DBF 폴더 및 `.STC` 패키지 선택·불러오기 (`COSTN.DBF`, `BOQ11/12.DBF`, `PRJLST.DBF`, `PRJLST.SMT`) + - `C:\경영정보(주)\System\ST_BSC\*.JB_R` 제비율 기준 DBF(행20·45·46 좌표) 로드 및 원가계산 요율 즉시 적용 + - `COSTN.DBF` 단가 직접 편집 및 시뮬레이터 계산 결과의 **원본 호환 DBF / `.STC` 파일 직접 쓰기(`tools/stmate-test/work_dbf/`)** 1. **01 기초자원 · 노임환산 (`ST_Sys.CPM`, `COSTN.DBF`)** - 자재·노무·경비 기초자원의 5개 가격지 후보(`AMT_11..51`: 조달·물자·물정·거래·견적) 입력 diff --git a/tools/stmate-test/app.js b/tools/stmate-test/app.js index 5d6e7d200..c60ca00a1 100644 --- a/tools/stmate-test/app.js +++ b/tools/stmate-test/app.js @@ -256,7 +256,8 @@ function renderAll() { }); const panel = $("tabPanel"); - if (activeTab === "resources") panel.innerHTML = renderResourcesTab(res); + if (activeTab === "db") panel.innerHTML = StmateDbBridge.renderDbTab(state, res); + else if (activeTab === "resources") panel.innerHTML = renderResourcesTab(res); else if (activeTab === "machines") panel.innerHTML = renderMachinesTab(res); else if (activeTab === "ilwi") panel.innerHTML = renderIlwiTab(res); else if (activeTab === "boq") panel.innerHTML = StmateCostViews.renderBoqTab(state, res); @@ -681,4 +682,17 @@ $("fileOpen").addEventListener("change", async (e) => { } }); +if (typeof StmateDbBridge !== "undefined") { + StmateDbBridge.initEvents( + () => state, + (newSt) => { + state = newSt; + selectedIlwiId = state.ilwiItems[0]?.id || ""; + }, + () => { + saveState(); + renderAll(); + }, + ); +} renderAll(); diff --git a/tools/stmate-test/db_bridge.js b/tools/stmate-test/db_bridge.js new file mode 100644 index 000000000..4dfafaf29 --- /dev/null +++ b/tools/stmate-test/db_bridge.js @@ -0,0 +1,345 @@ +/* -*- coding: utf-8 -*- */ +/** + * STMATE 원본 DBF / SMT / STC / JB_R 직접 읽기·쓰기 연동 컨트롤러 (db_bridge.js) + */ +const StmateDbBridge = (() => { + const API_BASE = "http://127.0.0.1:8095"; + const esc = (s) => + String(s ?? "").replace( + /[&<>"']/g, + (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c], + ); + const fmt = (n, maxDec = 2) => + new Intl.NumberFormat("ko-KR", { maximumFractionDigits: maxDec }).format(Number(n) || 0); + + const dbCtx = { + connected: false, + sources: { projects: [], jbr_files: [], work_root: "" }, + selectedProjectPath: "", + selectedJbrPath: "", + targetFolderName: "stmate_edited", + loadedDbf: null, + loadedJbr: null, + lastSaveResult: null, + statusMsg: "로컬 DB 브리지(127.0.0.1:8095) 연결 확인 중...", + }; + + async function checkAndLoadSources(onUpdate) { + try { + const resp = await fetch(`${API_BASE}/api/sources`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + dbCtx.connected = true; + dbCtx.sources = data; + if (!dbCtx.selectedProjectPath && data.projects.length > 0) { + dbCtx.selectedProjectPath = data.projects[0].path; + } + const onlyJbr = data.jbr_files.filter((f) => f.ext === ".JB_R"); + if (!dbCtx.selectedJbrPath && onlyJbr.length > 0) { + dbCtx.selectedJbrPath = onlyJbr[onlyJbr.length - 1].path; + } + dbCtx.statusMsg = `연결됨 — PC 프로젝트 DB ${data.projects.length}건 · ST_BSC 제비율 DB ${data.jbr_files.length}건 감지`; + } catch (_) { + dbCtx.connected = false; + dbCtx.statusMsg = + "오프라인 — 터미널에서 .\\venv\\Scripts\\python.exe tools\\stmate-test\\server.py 실행 시 DBF 직접 읽기/쓰기 활성화"; + } + if (onUpdate) onUpdate(); + } + + async function loadProjectDbf(applyStateCallback, onUpdate) { + if (!dbCtx.selectedProjectPath) return; + try { + dbCtx.statusMsg = `DBF/STC 읽는 중: ${dbCtx.selectedProjectPath} ...`; + if (onUpdate) onUpdate(); + const resp = await fetch(`${API_BASE}/api/load`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ source_path: dbCtx.selectedProjectPath }), + }); + if (!resp.ok) throw new Error(await resp.text()); + const data = await resp.json(); + dbCtx.loadedDbf = data; + dbCtx.lastSaveResult = null; + dbCtx.statusMsg = `로드 완료: ${data.prjlst?.PNAME || data.source_path} (COSTN ${data.costn_count}건, ${data.boq_table_name} ${data.boq_count}건)`; + if (data.sim_state && applyStateCallback) { + applyStateCallback(data.sim_state); + } + } catch (err) { + dbCtx.statusMsg = `DBF 로드 실패: ${err.message || err}`; + if (onUpdate) onUpdate(); + } + } + + async function loadAndApplyJbr(state, saveAndRender) { + if (!dbCtx.selectedJbrPath) return; + try { + const resp = await fetch(`${API_BASE}/api/load-jbr`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jbr_path: dbCtx.selectedJbrPath }), + }); + if (!resp.ok) throw new Error(await resp.text()); + const data = await resp.json(); + dbCtx.loadedJbr = data; + const r = data.extracted_rates; + Object.assign(state.kanjubConfig.rates, { + KANNO: r.KANNO || state.kanjubConfig.rates.KANNO, + KITA: r.KITA || state.kanjubConfig.rates.KITA, + ILBAN: r.ILBAN || state.kanjubConfig.rates.ILBAN, + SANBO: r.SANBO, + KOBO: r.KOBO, + KUNBO: r.KUNBO, + YUNBO: r.YUNBO, + SLVBO: r.SLVBO, + IYUN: r.IYUN, + ANJUN: r.ANJUN, + }); + state.kanjubConfig.anjunBaseAdd = r.anjunBaseAdd; + dbCtx.statusMsg = `ST_BSC 제비율 DB(${data.name}) 요율 적용 완료`; + if (saveAndRender) saveAndRender(); + } catch (err) { + dbCtx.statusMsg = `JB_R 로드 실패: ${err.message || err}`; + if (saveAndRender) saveAndRender(); + } + } + + async function saveToDbf(state, calcRes, onUpdate) { + const srcPath = dbCtx.loadedDbf?.source_path || dbCtx.selectedProjectPath; + if (!srcPath) return; + try { + const k = calcRes.kanjubOrig; + const costnUpdates = (dbCtx.loadedDbf?.costn_rows || []).slice(0, 80).map((r) => ({ + CODE: r.CODE, + NAME: r.NAME, + TAMT1: r.TAMT1, + LAMT1: r.LAMT1, + MAMT1: r.MAMT1, + SAMT1: r.SAMT1, + })); + // 시뮬레이터 기초자원 단가 변경분도 COSTN 업데이트에 병합 + for (const resItem of calcRes.resources.list) { + costnUpdates.push({ + CODE: resItem.code, + NAME: resItem.name, + TAMT1: resItem.adoptedPrice, + LAMT1: resItem.kind === "노무비" ? resItem.adoptedPrice : 0, + MAMT1: resItem.kind === "재료비" ? resItem.adoptedPrice : 0, + SAMT1: resItem.kind === "경비" ? resItem.adoptedPrice : 0, + }); + } + + const boqUpdates = calcRes.boqOrig.rows + .filter((r) => r._recno !== undefined) + .map((r) => ({ + _recno: r._recno, + NAME1: r.name, + QTY1: r.qty, + MAMT1: r.unitInfo.M, + LAMT1: r.unitInfo.L, + SAMT1: r.unitInfo.S, + TAMT1: r.unitInfo.T, + })); + + const prjlstUpdates = { + MCOST1: k.dM, + LCOST1: k.dL, + SCOST1: k.dS, + TCOST1: k.dT, + COSTW1: k.grandTotal, + }; + const rates = state.kanjubConfig.rates; + const smtUpdates = { + "1_R_KANNO": rates.KANNO, + "1_R_SANBO": rates.SANBO, + "1_R_KOBO": rates.KOBO, + "1_R_KUNBO": rates.KUNBO, + "1_R_SLVBO": rates.SLVBO, + "1_R_YUNBO": rates.YUNBO, + "1_R_ANJUN": rates.ANJUN, + "1_R_T_KITA": rates.KITA, + "1_R_HWAN": rates.HWAN, + "1_R_EQIPSU": rates.EQIP, + "1_R_ILBAN": rates.ILBAN, + "1_R_IYUN": rates.IYUN, + "1_C_KANNO": k.kanno, + "1_C_ILBAN": k.ilbanAmt, + "1_C_IYUN_M": k.pretaxProfitAdj, + "1_C_KONGSA_J": k.contractBeforeAdj, + "1_C_UPDNKWN": k.govFinal, + }; + + const resp = await fetch(`${API_BASE}/api/save`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source_path: srcPath, + target_folder_name: dbCtx.targetFolderName || "stmate_edited", + costn_updates: costnUpdates, + boq_updates: boqUpdates, + prjlst_updates: prjlstUpdates, + smt_updates: smtUpdates, + }), + }); + if (!resp.ok) throw new Error(await resp.text()); + const saved = await resp.json(); + dbCtx.lastSaveResult = saved; + dbCtx.statusMsg = `DBF·STC 저장 완료: ${saved.saved_stc} (${fmt(saved.stc_size, 0)} bytes)`; + await checkAndLoadSources(); + } catch (err) { + dbCtx.statusMsg = `DBF 저장 실패: ${err.message || err}`; + } + if (onUpdate) onUpdate(); + } + + function renderDbTab(state, calcRes) { + const projOpts = dbCtx.sources.projects + .map( + (p) => + ``, + ) + .join(""); + const jbrOpts = dbCtx.sources.jbr_files + .map( + (f) => + ``, + ) + .join(""); + + let dbfPreviewHtml = + '
상단에서 PC 내 STMATE 폴더 또는 .STC 파일을 선택하고 [원본 DBF/STC 불러오기]를 실행하세요.
'; + if (dbCtx.loadedDbf) { + const d = dbCtx.loadedDbf; + const costnRows = (d.costn_rows || []) + .slice(0, 25) + .map( + (r, i) => ` + + ${esc(r.CODE)} + + ${esc(r.SIZE)} + ${esc(r.UNIT)} + + + + + ${fmt(r.AMT_61, 0)} + `, + ) + .join(""); + + dbfPreviewHtml = ` +
+ 현재 연결 원본: ${esc(d.prjlst?.PNAME || d.source_path)} · 수록 테이블: ${esc(d.file_list.join(", "))} + 원장 COSTN.DBF ${d.costn_count}건 · 내역 ${esc(d.boq_table_name)} ${d.boq_count}건 · PRJLST.SMT 설정키 ${Object.keys(d.prjlst_smt || {}).length}개 +
+
+ + + ${costnRows} +
CODE품명(NAME)규격(SIZE)단위재료단가(MAMT1)노무단가(LAMT1)경비단가(SAMT1)합계단가(TAMT1)채택6칸(AMT_61)
+
`; + } + + const saveAlert = dbCtx.lastSaveResult + ? `
최근 쓰기 완료 폴더: ${esc(dbCtx.lastSaveResult.saved_dir)}생성된 패키지: ${esc(dbCtx.lastSaveResult.saved_stc)} (${fmt(dbCtx.lastSaveResult.stc_size, 0)} bytes, 파일 ${dbCtx.lastSaveResult.written_count}종)
` + : ""; + + return ` +
+
+
+

PC 내 STMATE 원본 DBF · SMT · STC · JB_R 직접 읽기/쓰기 연동

+

C:\\경영정보(주) 설치 폴더의 프로젝트 DBF(COSTN.DBF, BOQ11.DBF, PRJLST.DBF/.SMT), System\\ST_BSC\\*.JB_R 제비율 기준 DB, 그리고 실무 .STC 패키지를 직접 읽고 CP949 고정폭 바이너리 DBF로 다시 기록합니다.

+
+
+ +
+
+
+ 상태: ${esc(dbCtx.statusMsg)} + 브리지 주소: ${API_BASE} +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ ${saveAlert} +
+
+
+
+

원본 COSTN.DBF 레코드 직접 조회 및 편집 (상위 25건 미리보기)

+

여기서 수정한 단가와 01~06 탭에서 조작한 수량·요율·원가 결과가 [DBF 및 .STC로 직접 쓰기] 실행 시 COSTN.DBF, BOQ11/12.DBF, PRJLST.DBF/.SMT에 기록됩니다.

+
+
+ ${dbfPreviewHtml} +
`; + } + + function initEvents(getState, setState, saveAndRender) { + document.addEventListener("click", (e) => { + const btn = e.target.closest("[data-db-action]"); + if (!btn) return; + const act = btn.dataset.dbAction; + if (act === "refresh-sources") { + checkAndLoadSources(saveAndRender); + } else if (act === "load-project") { + loadProjectDbf((newState) => { + setState(newState); + saveAndRender(); + }, saveAndRender); + } else if (act === "load-jbr") { + loadAndApplyJbr(getState(), saveAndRender); + } else if (act === "save-dbf") { + const st = getState(); + saveToDbf(st, StmateCalc.calculateAll(st), saveAndRender); + } + }); + + document.addEventListener("change", (e) => { + const el = e.target; + if (el.id === "selDbProject") dbCtx.selectedProjectPath = el.value; + else if (el.id === "selDbJbr") dbCtx.selectedJbrPath = el.value; + else if (el.id === "inpTargetFolder") dbCtx.targetFolderName = el.value; + else if (el.dataset.dbf === "costn" && dbCtx.loadedDbf?.costn_rows) { + const idx = Number(el.dataset.idx); + const fld = el.dataset.field; + if (dbCtx.loadedDbf.costn_rows[idx]) { + dbCtx.loadedDbf.costn_rows[idx][fld] = el.value; + } + } + }); + + checkAndLoadSources(saveAndRender); + } + + return { + dbCtx, + initEvents, + checkAndLoadSources, + loadProjectDbf, + loadAndApplyJbr, + saveToDbf, + renderDbTab, + }; +})(); diff --git a/tools/stmate-test/index.html b/tools/stmate-test/index.html index 54f84b72b..9c1c832d3 100644 --- a/tools/stmate-test/index.html +++ b/tools/stmate-test/index.html @@ -7,6 +7,7 @@ + @@ -47,6 +48,7 @@