/* -*- 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, }; })();