diff --git a/resources/tester/test_stmate_conv_change.py b/resources/tester/test_stmate_conv_change.py index 5a5a45281..e10feaed0 100644 --- a/resources/tester/test_stmate_conv_change.py +++ b/resources/tester/test_stmate_conv_change.py @@ -1,7 +1,6 @@ -"""STMATE 재현 앱 분리형 변경 STC(차수 인계) · 코드 일괄 변경 · JB_* 기준 제비율표 · STC 검사. +"""STMATE 재현 앱 분리형 변경 STC(차수 인계) · 코드 일괄 변경 · STC 검사. -정답: 소광 원설계 STC(BOQ11·BOQ12) → 실제 「변경 1차분」 STC(16번 §4.6) · 원본 STC 바이트(코드 되돌림) · - 10번 §4 문서의 RATE ↔ JB 일치 사례(설치 폴더에 JB_* 가 있을 때만). +정답: 소광 원설계 STC(BOQ11·BOQ12) → 실제 「변경 1차분」 STC(16번 §4.6) · 원본 STC 바이트(코드 되돌림). """ import collections @@ -13,7 +12,6 @@ import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools/stmate-test")) from bdqty import decode_bdqty # noqa: E402 -from conv_jb import JB_DIR, list_jb, match_project # noqa: E402 from dbf_rw import DbfTable # noqa: E402 from smt_parser import extract_costn_memos, parse_dbf_bytes, parse_prjlst_smt # noqa: E402 from stc_archive import build_stc, check_stc # noqa: E402 @@ -117,57 +115,3 @@ def test_prjlst_rows_edit_other_project_row(): assert DbfTable(out["PRJLST.DBF"]).get(1, "PNAME") == "둘째 공사 이름 고침" out2, log2 = apply_save(f, {"prjlst_rows": rows}) assert log2 == [] and out2["PRJLST.DBF"] is f["PRJLST.DBF"] - - -needs_jb = pytest.mark.skipif(not JB_DIR.exists(), reason="STMATE 설치 폴더 JB_* 없음") - - -@needs_jb -def test_jb_files_shapes(): - files = list_jb() - assert len(files) >= 43 and all(f["shape_ok"] for f in files) - kinds = collections.Counter(f["kind"] for f in files) - assert ( - kinds["JB_1"] >= 1 and kinds["JB_M"] >= 1 and kinds["JB_R"] >= 27 and kinds["JB_RA"] >= 14 - ) - jr = {f["date"]: f["cols"] for f in files if f["kind"] == "JB_R"} - assert jr["2020-01-02"] == 20 and jr["2020-05-27"] == 35 # 2020-05-27 확장(10번 §4) - - -@needs_jb -@pytest.mark.parametrize( - "rel,table,date", - [ - ( - "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", - "RATE_R.DBF", - "2023-01-02", - ), - ( - "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", - "RATE.DBF", - "2011-01-12", - ), - ( - "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC", - "RATE_R.DBF", - "2024-03-15", - ), - ( - "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", - "RATE_R.DBF", - "2020-05-27", - ), - ], -) -def test_jb_match_documented_cases(rel, table, date): - m = match_project(Source(str(DOCS / rel)).files) - assert m[table]["exact"] and m[table]["date"] == date - - -@needs_jb -def test_jb_sogwang_five_cells(): - m = match_project(Source(str(ORIG)).files) - assert ( - m["RATE_R.DBF"]["date"] == "2025-01-01" and m["RATE_R.DBF"]["diff_cells"] == 5 - ) # 10번 §4 「5개 셀만 다름」 diff --git a/resources/tester/test_stmate_report_sheets.py b/resources/tester/test_stmate_report_sheets.py index 7e678e011..bf18d9a9d 100644 --- a/resources/tester/test_stmate_report_sheets.py +++ b/resources/tester/test_stmate_report_sheets.py @@ -193,5 +193,3 @@ def test_xlsx_export_writes_every_sheet_with_model_values(): same = abs(got - v) < 1e-9 if isinstance(v, float) else got == v assert same, f"{s['name']} {c}{i}" assert wb["〓 목 차 〓"]["D5"].hyperlink is not None - csv = client.post("/api/report/csv", json={"sheets": [sheets[2]], "filename": "c"}) - assert csv.status_code == 200 and "공사원가계산서" in csv.content.decode("utf-8-sig") diff --git a/resources/tester/test_stmate_stage_api.py b/resources/tester/test_stmate_stage_api.py index 0613c583e..10b28f01a 100644 --- a/resources/tester/test_stmate_stage_api.py +++ b/resources/tester/test_stmate_stage_api.py @@ -1,11 +1,9 @@ -"""STMATE 재현 앱 단계 서버(api_stage.py) — 공사별 원가설정 · 단계 보고서 엑셀. +"""STMATE 재현 앱 단계 서버(api_stage.py) — 단계 보고서 엑셀. -정답: 울진 2025 STC 계약 행(PCODE 12)의 PRJ_MEMO 를 smt_parser 로 따로 읽은 원가설정과 같음 · -엑셀은 보낸 시트 모형 값이 그대로 셀에 들어감(병합 머리 포함). +정답: 보낸 시트 모형 값이 그대로 셀에 들어감(병합 머리 포함). """ import io -import json import sys from pathlib import Path @@ -17,35 +15,12 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools/stmate-test")) sys.path.insert(0, str(Path(__file__).parent)) import api_stage # noqa: E402 -import server # noqa: E402 -from kanjub_cfg import build_kanjub_config # noqa: E402 -from smt_parser import parse_prjlst_smt # noqa: E402 -from test_stmate_kanjub_stc import DOCS, STCS # noqa: E402 app = FastAPI() app.include_router(api_stage.router) client = TestClient(app) -def test_projects_config_matches_memo(): - path = str(DOCS / STCS["울진2025"]) - res = client.post("/api/stage/projects", json={"source_path": path}) - assert res.status_code == 200 - projects = res.json()["projects"] - files = server.read_project_files(path) - load = server.api_load(server.LoadReq(source_path=path)) - assert [p["pcode"] for p in projects] == [r["PCODE"] for r in load["prjlst_all"]] - for p, r in zip(projects, load["prjlst_all"]): - smt = parse_prjlst_smt(files["PRJLST.SMT"], r["PRJ_MEMO_HEX"]) - assert p["stages"]["1"] == json.loads(json.dumps(build_kanjub_config(smt, 1))) - assert p["costs"]["COSTW1"] == r["COSTW1"] - - -def test_projects_bad_path(): - res = client.post("/api/stage/projects", json={"source_path": str(ROOT / "없는파일.STC")}) - assert res.status_code == 400 - - def test_export_xlsx_cells(): sheet = { "name": "설계변경증감대비표", diff --git a/tools/stmate-test/PLAN.md b/tools/stmate-test/PLAN.md index 5db345b06..d439fb99d 100644 --- a/tools/stmate-test/PLAN.md +++ b/tools/stmate-test/PLAN.md @@ -65,7 +65,7 @@ - [x] F1-30 할증 금액·율 두 갈래 편집 — ST_Jabpum Edt_Hal_Amt/Rate · 현재: 구현(할증 율·금액(기준단가 기준 · 두 번 해도 같은 값)) - [x] F1-31 가격지 후보 비교(1~5단가·최소단가) — wM_Boxa RG_MComp · 현재: 구현(가격지 후보 비교·채택 보호 행 제외(test_stmate_ui_protect)) - [x] F1-32 자재·공종 분류 필터, 불러간 목록 — wM_Boxa rg_FLT · wM_Box_H · 현재: 구현(속성·명칭·불러간 목록 필터) -- [x] F1-33 대가표 목록·선택(B/D/X) — wM_iLWi_San · 현재: 구현(view_dan_ilwi 236~) +- [x] F1-33 대가표 목록·선택(B/D/X) — wM_iLWi_San · 현재: 구현(scr_edit_san 대가표 목록·B/D/X 거르기) - [x] F1-34 일위대가 구성행 표 편집·노임계수·저장비목 선택 — wM_Edit_iLWi · 현재: 구현(구성행 편집 · 저장 = BDQTY 바뀐 호표만 암호로 다시 씀(stc_save_extra)) - [x] F1-35 단가산출근거 편집 + 계산(F9) — wM_Edit_San · 현재: 구현(산근 편집 + F9 · 함수·문법 검사(test_stmate_smt_lang)) - [x] F1-36 손료 계산(F6)·시간당 손료계수 수정 — wM_Edit_San · wM_Edit_XRate · 현재: 구현(REA 36 — 손료 계수 반올림 = MROUNDUD 선택 1(첫 버림 자리 ≥5 올림 · DP_Common 0x4544f0) · calc_xrate 원형 사용(test_stmate_xrate_round)) @@ -200,7 +200,7 @@ - [x] F4-12 COSTA.DBF 읽기 — 02번 · 현재: 구현(읽기(실무 표본 0건)) - [x] F4-13 BOQ1n 읽기·쓰기 — 02번 · 16번 · 현재: 구현(고친 칸만 · 2·3칸 보존 · 소계·TCOST 다시 셈 · 행 끼우기·지우기) - [x] F4-14 RATE*.DBF 읽기 — 06번 §1 · 현재: 구현(server.py 474) -- [x] F4-15 JB_* 기준요율표 43종 읽기(구형·2020 확장) — 10번 · 12번 §9 · 현재: 구현(conv_jb JB 요율표 판·모양 문서 일치 · 문서 일치 사례 재현) +- [x] F4-15 JB_* 기준요율표 43종 읽기(구형·2020 확장) — 10번 · 12번 §9 · 현재: 구현(제비율표 불러오기 = /api/load-jbr 로 JB_* 읽기 · 판 모양 검사·프로젝트 대조 보기는 원본에 없어 걷음) - [x] F4-16 KONGJONG.DBF 공종 분류 — 02번 · 현재: 구현(읽기(실무 표본 공종 1건 내외)) - [x] F4-17 SET_REP.DBF 서식 카탈로그 읽기 — 20번 §3 · 현재: 구현(표시) - [x] F4-18 QCODE.DBF·QCODE.SMT 읽기 — 02번 · 28번 · 현재: 구현(읽기(실무 표본 0건)) @@ -281,7 +281,7 @@ - [x] F6-21 예상적용율 찾기(목표금액 역산)·미리계산 — 27번 §2.3 · 현재: 구현(목표금액 → 적용율 87.745% 역산 · 미리계산) - [x] F6-22 설계 → 계약자료 변환등록(당초/변경·레벨) — wM_Conv_Mode · 현재: 구현(소광 원설계 → 변환등록·저장 = 실제 울진 계약 STC 일치(test_stmate_stage_register)) - [x] F6-23 낙찰율 열(NAGCD)·계약단가 저장 — 26번 §4 · 현재: 구현(새 코드 낙찰율·단가·산근 글·구성표 저장 = 울진 계약 STC 일치(test_stmate_stage_register)) -- [x] F6-24 계약 원가계산(계약 제잡비) — 27번 · 현재: 구현(계약 원가계산 · 울진 총공사비 333,315,000원 일치) +- [x] F6-24 계약 원가계산(계약 제잡비) — 27번 · 현재: 구현(계약 원가계산 · 울진 총공사비 333,315,000원 일치 · 저장 계약 공사 대조 단추는 원본에 없어 걷음) - [x] F6-25 계약·실행내역서(0623·0624) — PM_Nae_Cont · 현재: 구현(REA MCP 32 §8 계약·실행내역서 0623·0624 = 설계 내역서 인쇄 모듈을 지금 공사에 그대로 · 기본 내역서 열(0623 두 모듈 고르는 기준 미확인)) - [x] F6-26 실행예산 작업(실행단가·실행수량) — wBoq 실행예산작업 · 현재: 구현(REA 32 할증 0x511fa8 + 행 금액 0x456504 = 설계와 같은 공용 행 계산 엔진(test_stmate_stage_exec_rea)) - [x] F6-27 중기 실행단가 계산(절사 6택·차액 보정 순서) — wM_Boq_ExecX · 현재: 구현(REA 32 0x4da7a8 합계 = 사용료÷시간 1원 절사 · 최초단가 비례 · 끝전 OPTIMIZ · 차액 = OPTNM 첫 성분(test_stmate_stage_exec_rea)) diff --git a/tools/stmate-test/README.md b/tools/stmate-test/README.md index 64fbd4b9a..7f23b2dd7 100644 --- a/tools/stmate-test/README.md +++ b/tools/stmate-test/README.md @@ -21,10 +21,8 @@ | :--- | ---: | :--- | | [`index.html`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/index.html) | 179 | `TWM_STMATE_START_8X` 5대 상단 메뉴(`1. 프로젝트/초기값`, `2. 단가표/대가표`, `3. 내역서/인쇄`, `4. 도구`, `5. 기타`), 8대 메인 툴바, MDI 작업영역, 하단 실시간 집계 상태바(`St_Bar`) | | [`style.css`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/style.css) | 596 | Delphi 7 + Raize Components(`TRzBitBtn`, `TRzPageControl`) + `TRDbGrid` 클래식 윈도우 데스크톱 UI | -| [`smt_parser.py`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/smt_parser.py) | 375 | dBASE III/IV(`DBF`) 파서·빌더, `COSTN.DBF` 10바이트 바이너리 `IFORM`(` dict[str, str]: - """PRJ_MEMO 섹션들을 키=값 하나로 · 빈 값은 뺌(smt_parser 와 같게).""" - out: dict[str, str] = {} - for kv in sections.values(): - out.update({k: v for k, v in kv.items() if str(v).strip() != ""}) - return out - - -def project_configs(files: dict[str, bytes]) -> list[dict]: - """PRJLST 행마다 공사 정보 + 차수별 원가설정(build_kanjub_config).""" - rows = table_rows(files.get("PRJLST.DBF", b"")) - memos = {m["recno"]: m for m in prjlst_smt_all(files)} - out = [] - for r in rows: - kv = _flat(memos.get(r["_recno"], {}).get("sections", {})) - out.append( - { - "recno": r["_recno"], - "pcode": str(r.get("PCODE", "")).strip(), - "ccode": str(r.get("CCODE", "")).strip(), - "pclass": str(r.get("PCLASS", "")).strip(), - "k_ord": str(r.get("K_ORD", "")).strip(), - "pnum": r.get("PNUM", ""), - "pname": r.get("PNAME", ""), - "boq_table": f"BOQ{str(r.get('PCODE', '')).strip()}.DBF", - "has_boq": f"BOQ{str(r.get('PCODE', '')).strip()}.DBF" in files, - "costs": {k: v for k, v in r.items() if COST_COLS.match(k)}, - "stages": {n: build_kanjub_config(kv, n) for n in stage_slots(kv)} if kv else {}, - } - ) - return out - - -@router.post("/api/stage/projects") -def stage_projects(req: StagePathReq): - try: - src = Source(req.source_path) - except (FileNotFoundError, ValueError) as e: - raise HTTPException(status_code=400, detail=str(e)) - return {"source_path": req.source_path, "projects": project_configs(src.files)} - - def build_xlsx(sheets: list[StageSheet]) -> bytes: """시트 모형 → xlsx — 제목 · 안내 줄 · 머리(병합 {t,c,r}) · 값(숫자 #,##0 · 소수 4자리).""" wb = Workbook() diff --git a/tools/stmate-test/app.js b/tools/stmate-test/app.js index 7ee908c43..afb32fdee 100644 --- a/tools/stmate-test/app.js +++ b/tools/stmate-test/app.js @@ -7,8 +7,6 @@ const Calc = global.STMATE_CALC; const ViewPrjBoq = global.STMATE_VIEW_PRJ_BOQ; - const ViewDanIlwi = global.STMATE_VIEW_DAN_ILWI; - const ViewKanjubPrt = global.STMATE_VIEW_KANJUB_PRT; const { fmt, num } = Calc; const $ = (id) => document.getElementById(id), closeModal = () => $("stmate-modal-backdrop").classList.add("hidden"), @@ -21,7 +19,6 @@ state: Calc.createDefaultState(), computed: null, sourcesData: { projects: [], jbr_files: [], work_root: "" }, - catalogData: { codes: [], recipes: [], special_san: [] }, currentSourcePath: "", dbfData: null, uiState: { @@ -36,14 +33,6 @@ ilwiSubFilter: "ALL_BDX", selectedIlwiCode: "", editedSmtMap: {}, - kanRateTab: "RATE_R.DBF", - stageSubTab: "CONTRACT", - bidRateM: 87.745, - bidRateL: 87.745, - bidRateS: 87.745, - prevPct: 30.0, - progPct: 65.0, - activeReportSheet: "0608", }, }; @@ -86,28 +75,13 @@ }); } - const RENDERERS = { - wPrjList: ViewPrjBoq.renderWPrjList, - wBoq: ViewPrjBoq.renderWBoq, - wM_Set_Sys: ViewDanIlwi.renderWMSetSys, - wM_JabPum: ViewDanIlwi.renderWMJabPum, - wM_Edit_iLWi: ViewDanIlwi.renderWMEditIlwi, - wM_KanJub: ViewKanjubPrt.renderWMKanJub, - wM_KAN_RATE: ViewKanjubPrt.renderWMKanRate, - wStage: ViewKanjubPrt.renderWStage, - PM_Prt: ViewKanjubPrt.renderPMPrt, - }; - function renderWorkspace() { recompute(); renderChromeBars(); const ws = $("mdi-workspace"); if (!ws) return; - const scr = PLUG.screens[ctx.uiState.activeScreen]; - ws.innerHTML = scr - ? scr.render(ctx, API) - : (RENDERERS[ctx.uiState.activeScreen] || ViewPrjBoq.renderWPrjList)(ctx); - bindWorkspaceEvents(); + const scr = PLUG.screens[ctx.uiState.activeScreen] || PLUG.screens.wPrjList; + ws.innerHTML = scr ? scr.render(ctx, API) : ""; if (scr && scr.bind) scr.bind(ctx, API); } @@ -198,209 +172,11 @@ $("modal-title-text").textContent = title; $("modal-body-content").innerHTML = bodyHtml; $("stmate-modal-backdrop").classList.remove("hidden"); - bindModalEvents(); } - // 도구 창 바인딩·내역 행 끼워 넣기 — app_modals.js - const bindModalEvents = () => global.STMATE_APP_MODALS.bindModalEvents(API); + // 내역 행 끼워 넣기 — app_modals.js const insertBoqRow = (...args) => global.STMATE_APP_MODALS.insertBoqRow(API, ...args); - function bindWorkspaceEvents() { - const goScreen = (scr) => { - ctx.uiState.activeScreen = scr; - renderWorkspace(); - }; - - bindEach(".prj-row", "ondblclick", (tr) => { - const p = tr.getAttribute("data-path"); - p === ctx.currentSourcePath ? goScreen("wBoq") : loadProjectFromServer(p, "wBoq"); - }); - bindEach(".btn-load-prj", "onclick", (btn, e) => { - e.stopPropagation(); - const p = btn.getAttribute("data-path"); - p === ctx.currentSourcePath ? goScreen("wBoq") : loadProjectFromServer(p, "wPrjList"); - }); - "prj-btn-open-boq:wBoq,prj-btn-kanjub:wM_KanJub,kj-btn-open-rate:wM_KAN_RATE" - .split(",") - .forEach((p) => { - const [id, scr] = p.split(":"); - onClick(id, () => goScreen(scr)); - }); - ["boq-btn-save-dbf", "kj-btn-save-dbf"].forEach((id) => onClick(id, saveAllToDbfServer)); - onClick("prj-btn-toggle-preview", () => { - ctx.uiState.showBoqPreview = !ctx.uiState.showBoqPreview; - renderWorkspace(); - }); - onClick("prj-btn-edit-meta", () => { - const nextName = prompt("공사명(PNAME) 수정:", ctx.state.projectMeta.pname); - if (nextName) { - ctx.state.projectMeta.pname = nextName.trim(); - if (ctx.dbfData && ctx.dbfData.prjlst) ctx.dbfData.prjlst.PNAME = nextName.trim(); - renderWorkspace(); - } - }); - - // 내역서·초기값·단가목록표 화면 묶음은 scr_ui_boq.js · scr_ui_sys.js · scr_ui_dan.js - bindModalOpener("ilwi-btn-load-recipe", "Act_CatalogModal"); - - bindEach(".ilwi-sub-btn", "onclick", (btn) => { - ctx.uiState.ilwiSubFilter = btn.getAttribute("data-sub"); - renderWorkspace(); - }); - bindEach(".ilwi-master-row", "onclick", (tr) => { - ctx.uiState.selectedIlwiCode = tr.getAttribute("data-code"); - renderWorkspace(); - }); - const smtArea = $("smt-formula-textarea"); - if (smtArea) - smtArea.oninput = () => - (ctx.uiState.editedSmtMap[smtArea.getAttribute("data-code")] = smtArea.value); - onClick("ilwi-btn-apply-f9", () => { - const code = $("ilwi-btn-apply-f9").getAttribute("data-code"); - const txt = ($("smt-formula-textarea") || {}).value || ""; - ctx.uiState.editedSmtMap[code] = txt; - if (ctx.dbfData && ctx.dbfData.costn_memos) { - const prev = ctx.dbfData.costn_memos[code] || { code, blockNo: 1 }; - ctx.dbfData.costn_memos[code] = Object.assign({}, prev, { text: txt, byteLen: txt.length }); - } - const evalRes = ViewDanIlwi.evaluateSmtFormulaText(txt, ctx, code); - if (evalRes.sumT > 0) - ViewPrjBoq.syncCostnUnitPrice(ctx, code, evalRes.sumM, evalRes.sumL, evalRes.sumS); - cascadeAndRender(); - }); - - bindEach(".kj-scheme-tab", "onclick", (btn) => { - ctx.state.kanjubConfig.scheme = btn.getAttribute("data-scheme"); - renderWorkspace(); - }); - bindEach(".kj-rate-input", "onchange", (inp) => { - ctx.state.kanjubConfig.rates[inp.getAttribute("data-key")] = num(inp.value); - renderWorkspace(); - }); - [ - ["kj-opt-anjun-gov", "anjunGovVatExcluded", (v) => v === "1"], - ["kj-anjun-base", "anjunBaseAdd", (v) => num(v)], - ["kj-anjun-cap", "anjunCap12", (v) => v === "1"], - ["kj-ind-trunc", "indirectTruncDigits", (v) => parseInt(v, 10)], - ["kj-vat-mode", "vatMode", (v) => v], - ["kj-tot-jeol", "totJeolDigits", (v) => parseInt(v, 10)], - ["kj-profit-adj", "enableProfitEndAdj", (v) => v === "1"], - ["kj-gov-round", "govRoundMode", (v) => v], - ].forEach(([id, key, fn]) => - onChange(id, (e) => { - ctx.state.kanjubConfig[key] = fn(e.target.value); - renderWorkspace(); - }), - ); - onClick("kj-btn-apply-jbr", async () => { - const p = $("kj-jbr-select").value; - if (!p) return; - const d = await postJson("/api/load-jbr", { jbr_path: p }); - Object.assign(ctx.state.kanjubConfig.rates, d.extracted_rates); - if (d.extracted_rates.anjunBaseAdd) - ctx.state.kanjubConfig.anjunBaseAdd = d.extracted_rates.anjunBaseAdd; - renderWorkspace(); - }); - bindEach(".kr-tab-btn", "onclick", (btn) => { - ctx.uiState.kanRateTab = btn.getAttribute("data-tab"); - renderWorkspace(); - }); - onClick("kr-btn-load-jbr", async () => { - const p = $("kr-jbr-select").value; - if (!p) return; - const d = await postJson("/api/load-jbr", { jbr_path: p }); - if (ctx.dbfData && ctx.dbfData.rate_tables) { - ctx.dbfData.rate_tables["RATE_R.DBF"] = d.rows; - ctx.uiState.kanRateTab = "RATE_R.DBF"; - renderWorkspace(); - } - }); - bindEach(".stage-tab-btn", "onclick", (btn) => { - ctx.uiState.stageSubTab = btn.getAttribute("data-st"); - renderWorkspace(); - }); - onClick("st-btn-apply-bid", () => { - ["M:m", "L:l", "S:s"].forEach((p) => { - const [k, id] = p.split(":"); - ctx.uiState["bidRate" + k] = num($("st-bid-" + id).value, 87.745); - }); - renderWorkspace(); - }); - onClick("st-btn-apply-gisung", () => { - ctx.uiState.prevPct = num($("st-prev-pct").value, 30); - ctx.uiState.progPct = num($("st-prog-pct").value, 65); - renderWorkspace(); - }); - bindEach(".rev-edit-qty2", "onchange", (inp) => { - const item = ctx.state.boqItems.find((b) => b.id === inp.getAttribute("data-id")); - if (item) item.changedQty = num(inp.value); - renderWorkspace(); - }); - - onChange("prt-sheet-select", (e) => { - ctx.uiState.activeReportSheet = e.target.value; - renderWorkspace(); - }); - onClick("prt-btn-xls", () => exportFullXlsx()); - onClick("prt-btn-csv", () => { - const tbl = document.querySelector("#mdi-workspace table.dbgrid"); - if (!tbl) return; - const rows = Array.from(tbl.querySelectorAll("tr")).map((tr) => - Array.from(tr.querySelectorAll("th,td")) - .map((td) => `"${td.innerText.replace(/"/g, '""').trim()}"`) - .join(","), - ); - downloadBlob( - "\uFEFF" + rows.join("\r\n"), - `STMATE_Report_${ctx.uiState.activeReportSheet}.csv`, - "text/csv;charset=utf-8", - ); - }); - onClick("prt-btn-xml", () => { - const xmlStr = global.STMATE_ENGINE - ? global.STMATE_ENGINE.buildFullPpsXml(ctx) - : ""; - downloadBlob(xmlStr, "PPS_CostExchange_V105.xml", "application/xml;charset=utf-8"); - }); - onClick("prt-btn-jdl", () => { - const lines = (ctx.computed.boqComputed.rows || []).map( - (r) => `JDL|${r.code}|${r.name}|${r.spec}|${r.unit}|${r.qty}|${r.tUnit}|${r.tAmt}`, - ); - downloadBlob(lines.join("\r\n"), "STMATE_Export.JDL", "text/plain;charset=utf-8"); - }); - } - - async function exportFullXlsx() { - try { - const fullBoq = ViewPrjBoq.evaluateFullBoqRows(ctx); - const res = await fetch("/api/export-xlsx", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - prj_meta: ctx.state.projectMeta, - costn_rows: (ctx.dbfData && ctx.dbfData.costn_rows) || [], - boq_rows: fullBoq, - kanjub_data: ctx.computed.kanjubComputed, - costn_memos: (ctx.dbfData && ctx.dbfData.costn_memos) || {}, - recipes: (ctx.catalogData && ctx.catalogData.recipes) || [], - }), - }); - if (!res.ok) throw new Error(await res.text()); - downloadBlob( - await res.blob(), - `STMATE_Workbook_${ctx.state.projectMeta.pcode || "11"}.xlsx`, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ); - } catch (err) { - if (!global.STMATE_ENGINE) return alert("엑셀 내보내기 실패: " + err.message); - downloadBlob( - global.STMATE_ENGINE.buildMultiSheetExcelXml(ctx), - `STMATE_Workbook_${ctx.state.projectMeta.pcode || "11"}.xls`, - "application/vnd.ms-excel;charset=utf-8", - ); - } - } - function downloadBlob(content, filename, mime) { const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([content], { type: mime })); @@ -409,71 +185,22 @@ URL.revokeObjectURL(a.href); } - const MENU_SCREEN_MAP = { - Act_PrjList: "wPrjList", - Act_Init: "wM_Set_Sys", - Act_BoqEdit: "wBoq", - Act_KanJub: "wM_KanJub", - Act_KanRate: "wM_KAN_RATE", - Act_StageEdit: "wStage", - Act_Print: "PM_Prt", - }; - - const MENU_MODAL_MAP = { - Act_BoxaModal: ["5개 가격지 비교 (wM_Boxa)", "dan:wM_Boxa"], - Act_EdtHalModal: ["기준단가 비율 일괄수정 (wM_Edt_Hal)", "dan:wM_Edt_Hal"], - Act_CatalogModal: ["STMATE 표준 카탈로그 브라우저", "dan:CATALOG"], - Act_ITNumModal: ["내역서 공종번호 자동부여 (wITNum)", "boq:wITNum"], - Act_BoqRateModal: ["내역 할증율 적용 (wBoqRate)", "boq:wBoqRate"], - Act_MRateModal: ["비목 단수 일괄보정 (wM_Rate)", "boq:wM_Rate"], - Act_ExpndModal: ["일위대가 풀어넣기 (wExpnd)", "boq:wExpnd"], - Act_QtyModal: ["수량산출서 연결 집계 (wQtyList)", "boq:wQtyList"], - Act_ReaInfoModal: ["STMATE w 8.0 REA 정적분석 아키텍처 & DBF/SMT 명세", "rea:info"], - }; - - function openMappedModal(key) { - const [title, spec] = MENU_MODAL_MAP[key]; - const [grp, kind] = spec.split(":"); - openModal( - title, - grp === "dan" - ? ViewDanIlwi.buildDanModalHtml(kind, ctx) - : grp === "boq" - ? ViewPrjBoq.buildBoqToolModalHtml(kind, ctx) - : `

1. 실행 모듈: Start8x.exe · ST_Sub1.dll · ST_Prt.CPM

2. DBF/SMT 직결: COSTN.DBF · COSTN.SMT · BOQ11~14.DBF · PRJLST.DBF/.SMT · RATE_R/RA/M.DBF

`, - ); - } - - function bindModalOpener(id, key) { - onClick(id, () => openMappedModal(key)); - } - Object.assign(API, { renderWorkspace, recompute, openModal, postJson, downloadBlob }); Object.assign(API, { cascadeAndRender, loadProjectFromServer, insertBoqRow }); Object.assign(API, { saveAll: saveAllToDbfServer, decorateSavePayload, - openMappedModal, postSave, }); async function initStmateApp() { onClick("modal-close-btn", closeModal); - // 원본 메뉴·툴바·단축키 — ui_app_menu.js(끼워 넣은 메뉴·동작 포함) · 옛 동작 이름은 아래 표로 + // 원본 메뉴·툴바·단축키 — ui_app_menu.js(끼워 넣은 메뉴·동작 포함) global.STMATE_UI_APP_MENU.install(API); - $("main-menubar").addEventListener("click", (e) => { - const btn = e.target.closest("[data-act]"); - const act = btn && btn.getAttribute("data-act"); - if (!act || btn.disabled || PLUG.actions[act] || global.STMATE_UI_APP_MENU.H[act]) return; - if (MENU_SCREEN_MAP[act]) ctx.uiState.activeScreen = MENU_SCREEN_MAP[act]; - else if (MENU_MODAL_MAP[act]) return openMappedModal(act); - renderWorkspace(); - }); try { - const [srcRes, catRes] = await Promise.all([fetch("/api/sources"), fetch("/api/catalog")]); + const srcRes = await fetch("/api/sources"); if (srcRes.ok) ctx.sourcesData = await srcRes.json(); - if (catRes.ok) ctx.catalogData = await catRes.json(); const sel = $("top-project-select"); if (sel && ctx.sourcesData.projects.length) { sel.innerHTML = ctx.sourcesData.projects diff --git a/tools/stmate-test/app_modals.js b/tools/stmate-test/app_modals.js index b2011aa0d..f422cf701 100644 --- a/tools/stmate-test/app_modals.js +++ b/tools/stmate-test/app_modals.js @@ -1,34 +1,10 @@ /** - * STMATE 도구 창 공통 바인딩 · 내역 행 끼워 넣기 (app_modals.js) — api = window.STMATE_APP + * STMATE 내역 행 끼워 넣기 (app_modals.js) — api = window.STMATE_APP * 내역 도구(wITNum · wBoqRate · wM_Rate · wExpnd)는 scr_ui_boq_dlg.js · 가격지 비교(wM_Boxa)·기준단가 수정(wM_Edt_Hal)은 scr_ui_dan.js - * 여기 남은 것 = 수량산출서 연결(wQtyList 옛 창) · 표준 카탈로그 특수산식 불러오기 */ (function (global) { "use strict"; - function bindModalEvents(api) { - const { ctx, Calc, bindEach, closeModal, renderWorkspace } = api; - 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) || !ctx.dbfData) return; - const B = global.STMATE_UI_BOQ; - const model = { rows: ctx.dbfData.boq_rows, items: ctx.state.boqItems, deleted: [] }; - B.setCell(model, item._recno, "QTY1", Math.round(val * 100) / 100); - renderWorkspace(); - }); - bindEach(".btn-use-special-san", "onclick", (btn) => { - const spec = (ctx.catalogData.special_san || [])[parseInt(btn.getAttribute("data-idx"), 10)]; - if (!spec || !ctx.uiState.selectedIlwiCode) return; - ctx.uiState.editedSmtMap[ctx.uiState.selectedIlwiCode] = (spec["산식행"] || []).join("\r\n"); - ctx.uiState.activeScreen = "wM_Edit_iLWi"; - closeModal(); - renderWorkspace(); - }); - } - /** * 내역 행 끼워 넣기 — 선택 행 앞(선택 없으면 끝) · 저장 원천(boq_rows)·계산 원천(boqItems) 함께 * 코드가 단가목록에 있으면 명칭·규격·단위를 단가목록 값으로 @@ -60,5 +36,5 @@ return row; } - global.STMATE_APP_MODALS = { bindModalEvents, insertBoqRow }; + global.STMATE_APP_MODALS = { insertBoqRow }; })(window); diff --git a/tools/stmate-test/conv_jb.py b/tools/stmate-test/conv_jb.py deleted file mode 100644 index 7907fde03..000000000 --- a/tools/stmate-test/conv_jb.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -"""기준 제비율표 JB_* 43종 읽기(읽기 전용) — 설치 폴더 System\\ST_BSC\\*.JB_1·JB_M·JB_R·JB_RA. - -근거: 10_System_186개_파일분석장부.md §4(배포파일 ↔ DBF 구조 ↔ 프로젝트 표) · 12번 §9(5행 묶음 · 열 1~5 간접노무 · - 6~10 기타경비 · 2020-05-27 확장 = 뒤에 15열 덧붙임) · 파일 이름 '2026(상_0413)_제비율.JB_R' = 기준일 -판(edition): JB_1 41×10 · JB_M 25×10 · JB_R 구형 50×20 · JB_R 2020 확장 50×35 · JB_RA 50×20 -프로젝트 짝: JB_1 → RATE.DBF · JB_M → RATE_M.DBF · JB_R → RATE_R.DBF · JB_RA → RATE_RA.DBF -""" - -from __future__ import annotations - -import pathlib -import re - -from dbf_rw import DbfTable - -JB_DIR = pathlib.Path(r"C:\경영정보(주)\System\ST_BSC") -TABLE_OF = {"JB_1": "RATE.DBF", "JB_M": "RATE_M.DBF", "JB_R": "RATE_R.DBF", "JB_RA": "RATE_RA.DBF"} -SHAPES = {"JB_1": {(41, 10)}, "JB_M": {(25, 10)}, "JB_R": {(50, 20), (50, 35)}, "JB_RA": {(50, 20)}} - - -def _num(s: str) -> float: - try: - return float(s.strip() or 0) - except ValueError: - return 0.0 - - -def matrix_of(b: bytes) -> tuple[list[str], list[list[float]]]: - t = DbfTable(b) - return t.order, [[_num(t.get(i, f)) for f in t.order] for i in range(len(t))] - - -def base_date(name: str) -> str: - """'2026(상_0413,표준시장)' → '2026-04-13' · 월일 없으면 연도만.""" - m = re.match(r"^(\d{4})\([^_]*_?(\d{4})?", name) - if not m: - return "" - y, md = m.group(1), m.group(2) - return f"{y}-{md[:2]}-{md[2:]}" if md else y - - -def edition(kind: str, rows: int, cols: int) -> str: - if kind == "JB_R": - return "2020 확장(35열 · 국가유산 열)" if cols >= 35 else "구형(20열)" - return "단일판" - - -def read_jb(path) -> dict: - p = pathlib.Path(path) - kind = p.suffix.upper().lstrip(".") - fields, m = matrix_of(p.read_bytes()) - shape = (len(m), len(fields)) - return { - "name": p.name, - "path": str(p), - "kind": kind, - "table": TABLE_OF.get(kind, ""), - "date": base_date(p.stem), - "actual": "실적" in p.stem or "표준시장" in p.stem, - "rows": shape[0], - "cols": shape[1], - "edition": edition(kind, *shape), - "shape_ok": shape in SHAPES.get(kind, set()), - "fields": fields, - "matrix": m, - } - - -def list_jb(root: pathlib.Path | None = None) -> list[dict]: - """설치 폴더 JB_* 전부(행렬 제외 요약) — 종류·기준일 차례.""" - d = pathlib.Path(root or JB_DIR) - if not d.exists(): - return [] - out = [] - for p in sorted(d.glob("*.JB_*")): - r = read_jb(p) - r.pop("matrix") - out.append(r) - return sorted(out, key=lambda r: (r["kind"], r["date"], r["name"])) - - -def match_project(files: dict, root: pathlib.Path | None = None) -> dict: - """프로젝트 RATE*.DBF 마다 셀이 모두 같은 기준 JB 파일(없으면 가장 적게 다른 것과 다른 셀 수).""" - d = pathlib.Path(root or JB_DIR) - cands = [read_jb(p) for p in sorted(d.glob("*.JB_*"))] if d.exists() else [] - out = {} - for kind, table in TABLE_OF.items(): - if table not in files: - continue - _f, pm = matrix_of(files[table]) - best = None - for c in (x for x in cands if x["kind"] == kind): - if len(c["matrix"]) != len(pm): - continue - diff = sum( - 1 - for a, b in zip(c["matrix"], pm) - for j in range(max(len(a), len(b))) - if abs((a[j] if j < len(a) else 0.0) - (b[j] if j < len(b) else 0.0)) > 1e-9 - ) - if best is None or diff < best[0]: - best = (diff, c["name"], c["date"]) - if best: - out[table] = { - "jb": best[1], - "date": best[2], - "diff_cells": best[0], - "exact": best[0] == 0, - } - return out diff --git a/tools/stmate-test/excel_builder.py b/tools/stmate-test/excel_builder.py deleted file mode 100644 index 01407c3e9..000000000 --- a/tools/stmate-test/excel_builder.py +++ /dev/null @@ -1,496 +0,0 @@ -"""STMATE 호환 21시트 전체 엑셀 빌더 (excel_builder.py). - -- 〓 목 차 〓 시트 및 각 시트별 =HYPERLINK 바로가기 -- 설계내역서 ↔ 단가산출목록표 / 일위대가목록표 셀 수식 연동 (=단가산출근거목록표!F15) -- 700줄 제한 준수. -""" - -from io import BytesIO -from decimal import Decimal -import openpyxl -from openpyxl.styles import Font, Alignment, PatternFill, Border, Side -from openpyxl.utils import get_column_letter - - -def fnum(v, default=0.0): - try: - return float(Decimal(str(v or 0))) - except Exception: - return default - - -def inum(v, default=0): - try: - return int(Decimal(str(v or 0))) - except Exception: - return default - - -def build_stmate_full_xlsx( - prj_meta: dict, - costn_rows: list, - boq_rows: list, - kanjub_data: dict, - costn_memos: dict, - recipes: list, -) -> bytes: - wb = openpyxl.Workbook() - # remove default sheet - wb.remove(wb.active) - - thin_border = Border( - left=Side(style="thin", color="D0D5DD"), - right=Side(style="thin", color="D0D5DD"), - top=Side(style="thin", color="D0D5DD"), - bottom=Side(style="thin", color="D0D5DD"), - ) - header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid") - header_font = Font(name="맑은 고딕", size=9, bold=True, color="1E293B") - data_font = Font(name="맑은 고딕", size=9) - bold_font = Font(name="맑은 고딕", size=9, bold=True) - link_font = Font(name="맑은 고딕", size=9, color="1D4ED8", underline="single") - title_font = Font(name="맑은 고딕", size=14, bold=True) - - # 1. 〓 목 차 〓 - ws_idx = wb.create_sheet(title="〓 목 차 〓") - ws_idx.views.sheetView[0].showGridLines = True - ws_idx.cell( - row=2, column=2, value="공사명 : " + str(prj_meta.get("pname", "")) - ).font = title_font - ws_idx.cell(row=3, column=2, value="목 차").font = Font(name="맑은 고딕", size=12, bold=True) - - sheet_catalog = [ - ("공사원가계산서", "원가 총괄 요약표"), - ("설계내역서", "공종별 내역 및 금액 산출"), - ("일위대가목록표", "일위대가 호표 및 단가 일람"), - ("일위대가표", "일위대가 세부 구성 명세"), - ("단가산출근거목록표", "단가산출서 호표 및 단가 일람"), - ("단가산출근거", "단가산출 수식 및 인자 전개"), - ("중기목록표", "건설기계 사용료 목록"), - ("중기사용료", "중기 기계경비 세부 산정"), - ("재료비목록표", "자재 단가 및 규격"), - ("노무비목록표", "시중 노임 단가 및 직종"), - ("경비목록표", "기타 경비 항목 단가"), - ("환율및기초자료", "환율 및 기초 설정"), - ("〓 INITIAL 〓", "STMATE 계산 옵션 메타데이터"), - ] - - for i, (sname, desc) in enumerate(sheet_catalog, start=5): - ws_idx.cell(row=i, column=2, value=f"{i - 4}. {sname}").font = bold_font - ws_idx.cell(row=i, column=3, value=desc).font = data_font - cell_link = ws_idx.cell( - row=i, column=4, value=f'=HYPERLINK("#\'{sname}\'!A1", "{sname} →")' - ) - cell_link.font = link_font - - # 2. 공사원가계산서 - ws_won = wb.create_sheet(title="공사원가계산서") - ws_won.views.sheetView[0].showGridLines = True - ws_won.cell(row=1, column=1, value="공사원가계산서").font = title_font - ws_won.cell( - row=1, column=6, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - - w_headers = ["비목", "산정기초 금액(원)", "적용요율(%)", "산출금액(원)", "비고"] - for c, h in enumerate(w_headers, 1): - cell = ws_won.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center", vertical="center") - - w_lines = kanjub_data.get("lines", []) - for r, ln in enumerate(w_lines, start=4): - c1 = ws_won.cell(row=r, column=1, value=ln.get("label", "")) - c2 = ws_won.cell(row=r, column=2, value=inum(ln.get("base", 0))) - c3 = ws_won.cell(row=r, column=3, value=fnum(ln.get("rate", 0))) - c4 = ws_won.cell(row=r, column=4, value=inum(ln.get("amount", 0))) - c5 = ws_won.cell(row=r, column=5, value=ln.get("formula", "")) - c2.number_format = "#,##0" - c3.number_format = "0.000%" if fnum(ln.get("rate", 0)) < 1 else "0.00" - c4.number_format = "#,##0" - c1.alignment = Alignment(horizontal="left") - c2.alignment = c4.alignment = Alignment(horizontal="right") - c3.alignment = Alignment(horizontal="center") - for cell in (c1, c2, c3, c4, c5): - cell.font, cell.border = data_font, thin_border - - # 3. 일위대가목록표 & 4. 단가산출근거목록표 사전 맵 구축 - san_map = {} - ilwi_map = {} - d_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith("D")] - b_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith("B")] - x_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith("X")] - - ws_san_idx = wb.create_sheet(title="단가산출근거목록표") - ws_san_idx.views.sheetView[0].showGridLines = True - ws_san_idx.cell(row=1, column=1, value="단가산출근거목록표").font = title_font - ws_san_idx.cell( - row=1, column=9, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - - san_headers = [ - "순번", - "코드", - "품명", - "규격", - "단위", - "재료비", - "노무비", - "경비", - "합계단가", - "상세링크", - ] - for c, h in enumerate(san_headers, 1): - cell = ws_san_idx.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - - for idx, dr in enumerate(d_rows, start=4): - code = str(dr.get("CODE", "")).strip() - san_map[code] = idx - ws_san_idx.cell(row=idx, column=1, value=f"산근 {idx - 3}호표").alignment = Alignment( - horizontal="center" - ) - ws_san_idx.cell(row=idx, column=2, value=code).alignment = Alignment(horizontal="center") - ws_san_idx.cell(row=idx, column=3, value=dr.get("NAME", "")) - ws_san_idx.cell(row=idx, column=4, value=dr.get("SIZE", "")) - ws_san_idx.cell(row=idx, column=5, value=dr.get("UNIT", "")).alignment = Alignment( - horizontal="center" - ) - c_m = ws_san_idx.cell(row=idx, column=6, value=fnum(dr.get("MAMT1", 0))) - c_l = ws_san_idx.cell(row=idx, column=7, value=fnum(dr.get("LAMT1", 0))) - c_s = ws_san_idx.cell(row=idx, column=8, value=fnum(dr.get("SAMT1", 0))) - c_t = ws_san_idx.cell(row=idx, column=9, value=f"=F{idx}+G{idx}+H{idx}") - for cell in (c_m, c_l, c_s, c_t): - cell.number_format = "#,##0" - cell.alignment = Alignment(horizontal="right") - link_c = ws_san_idx.cell( - row=idx, - column=10, - value=f'=HYPERLINK("#\'단가산출근거\'!A{idx}", "{code} 상세 →")', - ) - link_c.font = link_font - for c in range(1, 11): - ws_san_idx.cell(row=idx, column=c).border = thin_border - if c != 10: - ws_san_idx.cell(row=idx, column=c).font = data_font - - ws_ilwi_idx = wb.create_sheet(title="일위대가목록표") - ws_ilwi_idx.views.sheetView[0].showGridLines = True - ws_ilwi_idx.cell(row=1, column=1, value="일위대가목록표").font = title_font - ws_ilwi_idx.cell( - row=1, column=9, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - - for c, h in enumerate(san_headers, 1): - cell = ws_ilwi_idx.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - - for idx, br in enumerate(b_rows, start=4): - code = str(br.get("CODE", "")).strip() - ilwi_map[code] = idx - ws_ilwi_idx.cell(row=idx, column=1, value=f"제 {idx - 3}호표").alignment = Alignment( - horizontal="center" - ) - ws_ilwi_idx.cell(row=idx, column=2, value=code).alignment = Alignment(horizontal="center") - ws_ilwi_idx.cell(row=idx, column=3, value=br.get("NAME", "")) - ws_ilwi_idx.cell(row=idx, column=4, value=br.get("SIZE", "")) - ws_ilwi_idx.cell(row=idx, column=5, value=br.get("UNIT", "")).alignment = Alignment( - horizontal="center" - ) - c_m = ws_ilwi_idx.cell(row=idx, column=6, value=fnum(br.get("MAMT1", 0))) - c_l = ws_ilwi_idx.cell(row=idx, column=7, value=fnum(br.get("LAMT1", 0))) - c_s = ws_ilwi_idx.cell(row=idx, column=8, value=fnum(br.get("SAMT1", 0))) - c_t = ws_ilwi_idx.cell(row=idx, column=9, value=f"=F{idx}+G{idx}+H{idx}") - for cell in (c_m, c_l, c_s, c_t): - cell.number_format = "#,##0" - cell.alignment = Alignment(horizontal="right") - link_c = ws_ilwi_idx.cell( - row=idx, - column=10, - value=f'=HYPERLINK("#\'일위대가표\'!A{idx}", "{code} 상세 →")', - ) - link_c.font = link_font - for c in range(1, 11): - ws_ilwi_idx.cell(row=idx, column=c).border = thin_border - if c != 10: - ws_ilwi_idx.cell(row=idx, column=c).font = data_font - - # 5. 설계내역서 (하이퍼링크 & 목록표 셀 참조 수식 연동) - ws_boq = wb.create_sheet(title="설계내역서") - ws_boq.views.sheetView[0].showGridLines = True - ws_boq.cell(row=1, column=1, value="설계내역서").font = title_font - ws_boq.cell( - row=1, column=13, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - - boq_headers = [ - "공종번호", - "공종 및 품명", - "규격", - "수량", - "단위", - "재료비단가", - "노무비단가", - "경비단가", - "합계단가", - "재료비금액", - "노무비금액", - "경비금액", - "합계금액", - "호표연결", - ] - for c, h in enumerate(boq_headers, 1): - cell = ws_boq.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - - for r_idx, br in enumerate(boq_rows, start=4): - code = str(br.get("BDCODE1", "")).strip() - itnum = str(br.get("ITNUM", "")).strip() - name = str(br.get("NAME1", "")).strip() - spec = str(br.get("SIZE1", "")).strip() - unit = str(br.get("UNIT1", "")).strip() - qty = fnum(br.get("QTY1", 0)) - is_sub = code.startswith("T") or code.startswith("E") or code.startswith("C") or not code - - ws_boq.cell(row=r_idx, column=1, value=itnum).alignment = Alignment(horizontal="center") - ws_boq.cell(row=r_idx, column=2, value=name).font = bold_font if is_sub else data_font - ws_boq.cell(row=r_idx, column=3, value=spec) - ws_boq.cell(row=r_idx, column=4, value=qty if not is_sub else "").number_format = "#,##0.00" - ws_boq.cell(row=r_idx, column=5, value=unit).alignment = Alignment(horizontal="center") - - if not is_sub and code in san_map: - s_row = san_map[code] - ws_boq.cell(row=r_idx, column=6, value=f"=단가산출근거목록표!F{s_row}") - ws_boq.cell(row=r_idx, column=7, value=f"=단가산출근거목록표!G{s_row}") - ws_boq.cell(row=r_idx, column=8, value=f"=단가산출근거목록표!H{s_row}") - link_txt = f"산근 {s_row - 3}호표" - ws_boq.cell( - row=r_idx, - column=14, - value=f'=HYPERLINK("#\'단가산출근거목록표\'!A{s_row}", "{link_txt} →")', - ).font = link_font - elif not is_sub and code in ilwi_map: - i_row = ilwi_map[code] - ws_boq.cell(row=r_idx, column=6, value=f"=일위대가목록표!F{i_row}") - ws_boq.cell(row=r_idx, column=7, value=f"=일위대가목록표!G{i_row}") - ws_boq.cell(row=r_idx, column=8, value=f"=일위대가목록표!H{i_row}") - link_txt = f"제 {i_row - 3}호표" - ws_boq.cell( - row=r_idx, - column=14, - value=f'=HYPERLINK("#\'일위대가목록표\'!A{i_row}", "{link_txt} →")', - ).font = link_font - else: - ws_boq.cell( - row=r_idx, - column=6, - value=fnum(br.get("MAMT1", 0)) if not is_sub else 0, - ) - ws_boq.cell( - row=r_idx, - column=7, - value=fnum(br.get("LAMT1", 0)) if not is_sub else 0, - ) - ws_boq.cell( - row=r_idx, - column=8, - value=fnum(br.get("SAMT1", 0)) if not is_sub else 0, - ) - ws_boq.cell(row=r_idx, column=14, value="") - - ws_boq.cell(row=r_idx, column=9, value=f"=F{r_idx}+G{r_idx}+H{r_idx}") - ws_boq.cell(row=r_idx, column=10, value=f"=ROUNDDOWN(D{r_idx}*F{r_idx}, 0)") # 재료비금액 - ws_boq.cell(row=r_idx, column=11, value=f"=ROUNDDOWN(D{r_idx}*G{r_idx}, 0)") # 노무비금액 - ws_boq.cell(row=r_idx, column=12, value=f"=ROUNDDOWN(D{r_idx}*H{r_idx}, 0)") # 경비금액 - ws_boq.cell(row=r_idx, column=13, value=f"=J{r_idx}+K{r_idx}+L{r_idx}") # 합계금액 - - for c in range(6, 14): - ws_boq.cell(row=r_idx, column=c).number_format = "#,##0" - ws_boq.cell(row=r_idx, column=c).alignment = Alignment(horizontal="right") - for c in range(1, 15): - ws_boq.cell(row=r_idx, column=c).border = thin_border - if c != 14: - ws_boq.cell(row=r_idx, column=c).font = bold_font if is_sub else data_font - - # 6. 기타 부속 시트들 (중기사용료, 자재, 노무, 경비, INITIAL) - _build_sub_sheets( - wb, - costn_rows, - costn_memos, - recipes, - thin_border, - header_fill, - header_font, - data_font, - bold_font, - link_font, - title_font, - ) - - # 자동 열 너비 조정 - for ws in wb.worksheets: - for col in ws.columns: - max_len = max(len(str(cell.value or "")) for cell in col[:40]) - col_letter = get_column_letter(col[0].column) - ws.column_dimensions[col_letter].width = min(max(max_len + 3, 10), 45) - - buf = BytesIO() - wb.save(buf) - return buf.getvalue() - - -def _build_sub_sheets( - wb, - costn_rows, - costn_memos, - recipes, - thin_border, - header_fill, - header_font, - data_font, - bold_font, - link_font, - title_font, -): - # 중기사용료 - ws_x = wb.create_sheet(title="중기사용료") - ws_x.views.sheetView[0].showGridLines = True - ws_x.cell(row=1, column=1, value="중기사용료").font = title_font - ws_x.cell(row=1, column=8, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")').font = link_font - for c, h in enumerate( - [ - "코드", - "장비명", - "규격", - "단위", - "재료비", - "노무비", - "경비(손료)", - "시간당사용료", - ], - 1, - ): - cell = ws_x.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - x_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith("X")] - for r_idx, xr in enumerate(x_rows, start=4): - ws_x.cell(row=r_idx, column=1, value=xr.get("CODE", "")).alignment = Alignment( - horizontal="center" - ) - ws_x.cell(row=r_idx, column=2, value=xr.get("NAME", "")) - ws_x.cell(row=r_idx, column=3, value=xr.get("SIZE", "")) - ws_x.cell(row=r_idx, column=4, value=xr.get("UNIT", "")).alignment = Alignment( - horizontal="center" - ) - for c, k in enumerate(["MAMT1", "LAMT1", "SAMT1", "TAMT1"], 5): - cell = ws_x.cell(row=r_idx, column=c, value=fnum(xr.get(k, 0))) - cell.number_format = "#,##0" - cell.alignment = Alignment(horizontal="right") - for c in range(1, 9): - ws_x.cell(row=r_idx, column=c).border = thin_border - ws_x.cell(row=r_idx, column=c).font = data_font - - # 단가산출근거 상세 - ws_d_det = wb.create_sheet(title="단가산출근거") - ws_d_det.views.sheetView[0].showGridLines = True - ws_d_det.cell(row=1, column=1, value="단가산출근거 세부").font = title_font - ws_d_det.cell( - row=1, column=6, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - for c, h in enumerate( - ["호표코드", "공종명", "규격", "단위", "합계단가", "COSTN.SMT 산출식 본문"], - 1, - ): - cell = ws_d_det.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - d_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith("D")] - for r_idx, dr in enumerate(d_rows, start=4): - code = str(dr.get("CODE", "")).strip() - ws_d_det.cell(row=r_idx, column=1, value=code).alignment = Alignment(horizontal="center") - ws_d_det.cell(row=r_idx, column=2, value=dr.get("NAME", "")) - ws_d_det.cell(row=r_idx, column=3, value=dr.get("SIZE", "")) - ws_d_det.cell(row=r_idx, column=4, value=dr.get("UNIT", "")).alignment = Alignment( - horizontal="center" - ) - cell_t = ws_d_det.cell(row=r_idx, column=5, value=fnum(dr.get("TAMT1", 0))) - cell_t.number_format = "#,##0" - cell_t.alignment = Alignment(horizontal="right") - m_txt = costn_memos.get(code, {}).get("text", "") - ws_d_det.cell(row=r_idx, column=6, value=m_txt.replace("\r\n", " | ")) - for c in range(1, 7): - ws_d_det.cell(row=r_idx, column=c).border = thin_border - ws_d_det.cell(row=r_idx, column=c).font = data_font - - # 일위대가표 상세 - ws_b_det = wb.create_sheet(title="일위대가표") - ws_b_det.views.sheetView[0].showGridLines = True - ws_b_det.cell(row=1, column=1, value="일위대가 세부 명세").font = title_font - ws_b_det.cell( - row=1, column=8, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - for c, h in enumerate( - ["호표코드", "품명", "규격", "단위", "수량", "재료비단가", "노무비단가", "경비단가"], - 1, - ): - cell = ws_b_det.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - b_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith("B")] - for r_idx, br in enumerate(b_rows, start=4): - ws_b_det.cell(row=r_idx, column=1, value=br.get("CODE", "")).alignment = Alignment( - horizontal="center" - ) - ws_b_det.cell(row=r_idx, column=2, value=br.get("NAME", "")) - ws_b_det.cell(row=r_idx, column=3, value=br.get("SIZE", "")) - ws_b_det.cell(row=r_idx, column=4, value=br.get("UNIT", "")).alignment = Alignment( - horizontal="center" - ) - ws_b_det.cell(row=r_idx, column=5, value=1).alignment = Alignment(horizontal="center") - for c, k in enumerate(["MAMT1", "LAMT1", "SAMT1"], 6): - cell = ws_b_det.cell(row=r_idx, column=c, value=fnum(br.get(k, 0))) - cell.number_format = "#,##0" - cell.alignment = Alignment(horizontal="right") - for c in range(1, 9): - ws_b_det.cell(row=r_idx, column=c).border = thin_border - ws_b_det.cell(row=r_idx, column=c).font = data_font - - # 자재/노무/경비 목록표 - for pfx, s_title, col_lbl in [ - ("M", "재료비목록표", "자재명"), - ("L", "노무비목록표", "직종명"), - ("S", "경비목록표", "경비항목명"), - ]: - ws_sub = wb.create_sheet(title=s_title) - ws_sub.views.sheetView[0].showGridLines = True - ws_sub.cell(row=1, column=1, value=s_title).font = title_font - ws_sub.cell( - row=1, column=7, value='=HYPERLINK("#\'〓 목 차 〓\'!B2", "목차 →")' - ).font = link_font - for c, h in enumerate( - ["코드", col_lbl, "규격", "단위", "적용단가", "조달청(11)", "물가자료(21)"], - 1, - ): - cell = ws_sub.cell(row=3, column=c, value=h) - cell.fill, cell.font, cell.border = header_fill, header_font, thin_border - cell.alignment = Alignment(horizontal="center") - p_rows = [r for r in costn_rows if str(r.get("CODE", "")).strip().startswith(pfx)] - for r_idx, pr in enumerate(p_rows, start=4): - ws_sub.cell(row=r_idx, column=1, value=pr.get("CODE", "")).alignment = Alignment( - horizontal="center" - ) - ws_sub.cell(row=r_idx, column=2, value=pr.get("NAME", "")) - ws_sub.cell(row=r_idx, column=3, value=pr.get("SIZE", "")) - ws_sub.cell(row=r_idx, column=4, value=pr.get("UNIT", "")).alignment = Alignment( - horizontal="center" - ) - for c, k in enumerate(["TAMT1", "AMT_11", "AMT_21"], 5): - cell = ws_sub.cell(row=r_idx, column=c, value=fnum(pr.get(k, 0))) - cell.number_format = "#,##0" - cell.alignment = Alignment(horizontal="right") - for c in range(1, 8): - ws_sub.cell(row=r_idx, column=c).border = thin_border - ws_sub.cell(row=r_idx, column=c).font = data_font diff --git a/tools/stmate-test/index.html b/tools/stmate-test/index.html index af368066c..9869bef95 100644 --- a/tools/stmate-test/index.html +++ b/tools/stmate-test/index.html @@ -37,7 +37,7 @@
- +
${OPTIONS.map( ([k, l]) => ``, @@ -220,7 +216,6 @@ ${inp("ct-target", o.target, 130)} 계약 이윤율(%)${inp("ct-iyun", o.iyunRate, 70)} - ${o.storedCfg ? `` : ""} ${o.found ? `찾은 적용율 ${o.found.rate}% → ${S.fmt(o.found.value)}원` : ""}
`; const pv = S.grid( [["적용금액 미리계산", "재료비", "노무비", "경비", "계"]], @@ -307,7 +302,6 @@ api.onChange("ct-what", (e) => (o.what = e.target.value)); api.onChange("ct-target", (e) => (o.target = e.target.value)); api.onChange("ct-iyun", (e) => ((o.iyunRate = e.target.value.trim()), re())); - api.onChange("ct-usestored", (e) => ((o.useStored = e.target.checked), re())); api.bindEach( ".ct-opt", "onchange", @@ -353,32 +347,6 @@ alert("변환등록 실패: " + e.message); } }); - api.onClick("ct-stored", async () => { - if (!ctx.currentSourcePath) return alert("먼저 공사를 불러옴"); - try { - const res = await api.postJson("/api/stage/projects", { - source_path: ctx.currentSourcePath, - }); - const c = compute(ctx); - const ctr = res.projects.find( - (p) => p.pcode !== String(ctx.state.projectMeta.pcode || "11"), - ); - if (!ctr) return alert("이 STC 에 저장된 계약(다른 PCODE) 공사 없음"); - o.storedCfg = ctr.stages[1]; - const rows = [ - ["직접공사비", n0(ctr.costs.TCOST1), c.conv.totals.directTotal], - ["총공사비", n0(ctr.costs.COSTW1), c.kj.grandTotal], - ]; - api.openModal( - `저장된 계약 공사 대조 — PCODE ${ctr.pcode} ${ctr.pnum || ""}`, - S.grid([["항목", "저장값", "재현값"]], rows, { style: "" }) + - `

「저장된 계약 원가설정 사용」을 켜면 원가계산서에 그 공사의 설정(이윤율·당초액 유지 표시)을 씀

`, - ); - re(); - } catch (e) { - alert("대조 실패: " + e.message); - } - }); S.bindReports(ctx, api, builders(ctx)); } diff --git a/tools/stmate-test/scr_ui_boq.js b/tools/stmate-test/scr_ui_boq.js index ac3ead39b..2ec7095ae 100644 --- a/tools/stmate-test/scr_ui_boq.js +++ b/tools/stmate-test/scr_ui_boq.js @@ -138,11 +138,7 @@ } else Object.assign(ctx.uiState, { selectedIlwiCode: c, activeScreen: "wM_Edit_iLWi" }); api.renderWorkspace(); }, - qtyList: (ctx, api) => { - if (P.screens.wQtyList) - return ((ctx.uiState.activeScreen = "wQtyList"), api.renderWorkspace()); - api.openMappedModal("Act_QtyModal"); - }, + qtyList: (ctx, api) => ((ctx.uiState.activeScreen = "wQtyList"), api.renderWorkspace()), }; function shift(ctx, api, d) { diff --git a/tools/stmate-test/server.py b/tools/stmate-test/server.py index 6d7500f98..92692d10d 100644 --- a/tools/stmate-test/server.py +++ b/tools/stmate-test/server.py @@ -8,7 +8,6 @@ import pathlib import re from bdqty import decode_bdqty -from excel_builder import build_stmate_full_xlsx from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import Response @@ -17,7 +16,6 @@ from kanjub_cfg import build_kanjub_config, stage_slots from pydantic import BaseModel from smt_parser import ( extract_costn_memos, - load_stmate_catalog, parse_dbf_bytes, parse_ini_bytes, parse_prjlst_smt, @@ -455,11 +453,6 @@ def api_sources(): return discover_sources() -@app.get("/api/catalog") -def api_catalog(): - return load_stmate_catalog() - - @app.post("/api/load") def api_load(req: LoadReq): files = read_project_files(req.source_path) @@ -611,35 +604,6 @@ def api_save(req: SaveDbfReq): return res -class ExportXlsxReq(BaseModel): - prj_meta: dict = {} - costn_rows: list = [] - boq_rows: list = [] - kanjub_data: dict = {} - costn_memos: dict = {} - recipes: list = [] - - -@app.post("/api/export-xlsx") -def api_export_xlsx(req: ExportXlsxReq): - try: - xlsx_bytes = build_stmate_full_xlsx( - req.prj_meta, - req.costn_rows, - req.boq_rows, - req.kanjub_data, - req.costn_memos, - req.recipes, - ) - return Response( - content=xlsx_bytes, - media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - headers={"Content-Disposition": 'attachment; filename="STMATE_Workbook.xlsx"'}, - ) - except Exception as e: - 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) diff --git a/tools/stmate-test/smt_engine.js b/tools/stmate-test/smt_engine.js index fa511cb5c..3edcc2174 100644 --- a/tools/stmate-test/smt_engine.js +++ b/tools/stmate-test/smt_engine.js @@ -1,12 +1,12 @@ /** - * STMATE w 8.0 — SIxMemo(COSTN.SMT) 산근 연결 · 5단 연쇄재계산(Act_Calc_All) · 다중시트 엑셀/조달XML/JDL 변환기 (smt_engine.js) - * 근거: 17_단가조립_검증.md, 18_누락_계산로직_점검.md, 23_COSTN_SMT_전수파싱.md, 28_변환_수량산출_식언어.md, 31_일위대가_레시피_사전.md + * STMATE w 8.0 — SIxMemo(COSTN.SMT) 산근 연결 · 5단 연쇄재계산(Act_Calc_All) (smt_engine.js) + * 근거: 17_단가조립_검증.md, 18_누락_계산로직_점검.md, 23_COSTN_SMT_전수파싱.md, 28_변환_수량산출_식언어.md */ (function (global) { "use strict"; const Calc = global.STMATE_CALC; - const { fmt, num } = Calc; + const { num } = Calc; const SmtEval = global.STMATE_SMT_EVAL; const rowCode = (r) => String(r.CODE || "").trim(); @@ -291,261 +291,6 @@ return count; } - /** - * 3. 선택 호표(B/X)의 표준 레시피 구성행 조회 - */ - function getRecipeBreakdown(ctx, code) { - const crows = (ctx.dbfData && ctx.dbfData.costn_rows) || []; - const row = crows.find((r) => String(r.CODE || "").trim() === code); - if (!row) return []; - const nm = String(row.NAME || "").trim(); - const sz = String(row.SIZE || "").trim(); - const p = code[0].toUpperCase(); - const recipes = (ctx.catalogData && ctx.catalogData.recipes) || []; - const kind = p === "X" ? "중기" : p === "B" ? "일위대가" : "단가산출"; - const pool = recipes.filter((r) => r["종류"] === kind); - const rec = - pool.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) || - pool.find((r) => r["명칭"].trim() === nm); - return rec && rec["구성"] ? rec["구성"] : []; - } - - /** - * 4. 다중 시트 Excel SpreadsheetML (.xls) 통합 워크북 생성기 (갑지·원가계산서·설계내역서·일위대가·단가산출·중기·단가목록) - */ - function buildMultiSheetExcelXml(ctx) { - const esc = (v) => - String(v ?? "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); - const kj = ctx.computed.kanjubComputed; - const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx); - const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || []; - const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; - - const mkSheet = (name, headers, rows) => ` - - - ${headers.map((h) => `${esc(h)}`).join("")} - ${rows - .map( - (r) => - `${r - .map((c) => { - const isN = typeof c === "number" && Number.isFinite(c); - return `${esc(c)}`; - }) - .join("")}`, - ) - .join("\n")} -
-
`; - - const shWon = mkSheet( - "공사원가계산서", - ["비목", "산정기초(원)", "요율(%)", "금액(원)", "산출근거"], - kj.lines.map((l) => [l.label, l.base || 0, l.rate || 0, l.amount || 0, l.formula || ""]), - ); - const shBoq = mkSheet( - "설계내역서", - [ - "공종번호", - "코드", - "공종명", - "규격", - "단위", - "수량", - "재료비단가", - "노무비단가", - "경비단가", - "합계단가", - "합계금액", - ], - boqRows.map((r) => [ - r.ITNUM || "", - r.BDCODE1 || "", - r.NAME1 || "", - r.SIZE1 || "", - r.UNIT1 || "", - num(r.QTY1), - num(r.MAMT1), - num(r.LAMT1), - num(r.SAMT1), - num(r.TAMT1), - num(r.T_AMT1), - ]), - ); - const shIlwi = mkSheet( - "일위대가목록표", - ["코드", "품명", "규격", "단위", "재료비", "노무비", "경비", "합계단가"], - costnRows - .filter((r) => String(r.CODE || "").startsWith("B")) - .map((r) => [ - r.CODE, - r.NAME, - r.SIZE, - r.UNIT, - num(r.MAMT1), - num(r.LAMT1), - num(r.SAMT1), - num(r.TAMT1), - ]), - ); - const shSan = mkSheet( - "단가산출근거", - [ - "코드", - "품명", - "규격", - "단위", - "재료비", - "노무비", - "경비", - "합계단가", - "산출근거(COSTN.SMT)", - ], - costnRows - .filter((r) => String(r.CODE || "").startsWith("D") || memos[r.CODE]) - .map((r) => [ - r.CODE, - r.NAME, - r.SIZE, - r.UNIT, - num(r.MAMT1), - num(r.LAMT1), - num(r.SAMT1), - num(r.TAMT1), - memos[r.CODE] ? memos[r.CODE].text.replace(/\r?\n/g, " | ") : "", - ]), - ); - const shJoong = mkSheet( - "중기사용료", - [ - "코드", - "장비명", - "규격", - "단위", - "재료비(연료/잡품)", - "노무비(운전원)", - "경비(손료)", - "시간당사용료", - ], - costnRows - .filter((r) => String(r.CODE || "").startsWith("X")) - .map((r) => [ - r.CODE, - r.NAME, - r.SIZE, - r.UNIT, - num(r.MAMT1), - num(r.LAMT1), - num(r.SAMT1), - num(r.TAMT1), - ]), - ); - const shDan = mkSheet( - "단가목록표", - [ - "코드", - "구분", - "품명", - "규격", - "단위", - "재료비", - "노무비", - "경비", - "적용단가", - "조달단가", - "물가자료", - ], - costnRows - .filter((r) => /^[LMS]/.test(String(r.CODE || ""))) - .map((r) => [ - r.CODE, - r.CODE[0], - r.NAME, - r.SIZE, - r.UNIT, - num(r.MAMT1), - num(r.LAMT1), - num(r.SAMT1), - num(r.TAMT1), - num(r.AMT_11), - num(r.AMT_21), - ]), - ); - - return ` - - - ${shWon} - ${shBoq} - ${shIlwi} - ${shSan} - ${shJoong} - ${shDan} -`; - } - - /** - * 5. 조달청 공사원가 호환규정 V1.0.5 (Valid_Xml.vsl · T3_C3B) 전체 계층 XML 생성기 - */ - function buildFullPpsXml(ctx) { - const esc = (v) => - String(v ?? "") - .replace(/&/g, "&") - .replace(//g, ">"); - const kj = ctx.computed.kanjubComputed; - const bt = ctx.computed.boqComputed.totals; - const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx); - const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || []; - - return ` - -
- STMATE w 8.0 (경영정보) - ${esc(ctx.state.projectMeta.pcode)} - ${esc(ctx.state.projectMeta.pname)} - ${esc(ctx.state.projectMeta.pnum)} - ${bt.directM} - ${bt.directL} - ${bt.directS} - ${bt.directT} - ${kj.FINAL_SUPPLY} - ${kj.GOV_MAT} - ${kj.GRAND_TOTAL} -
- - ${kj.lines - .map( - (ln) => - ` `, - ) - .join("\n")} - - - ${boqRows - .map( - (r) => - ` `, - ) - .join("\n")} - - - ${costnRows - .map( - (c) => - ` `, - ) - .join("\n")} - -
`; - } - global.STMATE_ENGINE = { evaluateSmtFormulaText, cascadeRecalculateAll, @@ -555,8 +300,5 @@ qtyResolver, laborMap, findCycles, - getRecipeBreakdown, - buildMultiSheetExcelXml, - buildFullPpsXml, }; })(window); diff --git a/tools/stmate-test/smt_parser.py b/tools/stmate-test/smt_parser.py index 638c0da21..1cf7c1812 100644 --- a/tools/stmate-test/smt_parser.py +++ b/tools/stmate-test/smt_parser.py @@ -1,23 +1,16 @@ # -*- coding: utf-8 -*- -"""STMATE dBASE III/IV(DBF) · SIxMemo(COSTN.SMT / PRJLST.SMT) 바이너리 파서·빌더 및 표준 카탈로그 로더. +"""STMATE dBASE III/IV(DBF) · SIxMemo(COSTN.SMT / PRJLST.SMT) 바이너리 파서·빌더. 무손실 고쳐 쓰기는 dbf_rw(DBF 칸) · dbf_memo(SIxMemo 블록) · stc_save(저장) 를 씀 — build_dbf_bytes 는 새 표 만들 때만. """ from __future__ import annotations -import csv import datetime import io -import json -import pathlib import re import struct -REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] -ANAL_DIR = REPO_ROOT / "resources" / "knowledge" / "original" / "경쟁사 프로그램" / "STmate 분석" -RAW_30_DIR = ANAL_DIR / "30_원자료" - RE_REF = re.compile(r"&([A-Z]\d{4,5})([LMS])?&") RE_PA = re.compile(r"&P([A-Z]\d{2}):([LMS])&") RE_ASSIGN = re.compile(r"\b([A-Za-z]\w*)\s*(?:'[^']*')?\s*=\s*([^,\r\n'=]+)") @@ -292,53 +285,6 @@ def rebuild_costn_with_smt( return out["COSTN.DBF"], out.get("COSTN.SMT", smt_bytes) -_CATALOG_CACHE: dict | None = None - - -def load_stmate_catalog() -> dict: - """30_원자료의 표준 코드(894종), 일위대가 레시피(350종), 특수 단가산출(21종) 로드.""" - global _CATALOG_CACHE - if _CATALOG_CACHE is not None: - return _CATALOG_CACHE - - codes: list[dict] = [] - csv_path = RAW_30_DIR / "stmate_code_name_list.csv" - if csv_path.exists(): - with csv_path.open("r", encoding="utf-8-sig", errors="replace") as f: - for row in csv.DictReader(f): - c = (row.get("code") or "").strip() - if c: - codes.append( - { - "code": c, - "name": (row.get("name") or "").strip(), - "size": (row.get("size") or "").strip(), - "unit": (row.get("unit") or "").strip(), - } - ) - - recipes = ( - json.loads((RAW_30_DIR / "일위대가_레시피.json").read_text(encoding="utf-8")) - if (RAW_30_DIR / "일위대가_레시피.json").exists() - else [] - ) - special_san = ( - json.loads((RAW_30_DIR / "특수_단가산출_21개.json").read_text(encoding="utf-8")) - if (RAW_30_DIR / "특수_단가산출_21개.json").exists() - else [] - ) - - _CATALOG_CACHE = { - "code_count": len(codes), - "codes": codes, - "recipe_count": len(recipes), - "recipes": recipes, - "special_san_count": len(special_san), - "special_san": special_san, - } - return _CATALOG_CACHE - - def parse_ini_bytes(b: bytes) -> dict[str, str]: """Start8x.Ini / STini.MEM 키=값(섹션 무시) — 산근 계산 방식(CHGNM) · 차액 보정 순서(OPTNM) 등.""" out: dict[str, str] = {} diff --git a/tools/stmate-test/view_dan_ilwi.js b/tools/stmate-test/view_dan_ilwi.js deleted file mode 100644 index b7a39635e..000000000 --- a/tools/stmate-test/view_dan_ilwi.js +++ /dev/null @@ -1,504 +0,0 @@ -/** - * STMATE w 8.0 — 초기값(wM_Set_Sys) · 단가목록표(wM_JabPum) · 대가표/산근 편집기(wM_Edit_iLWi · wM_Edit_San · COSTN.SMT) - * 근거: 22_UI_폼_분석.md, 23_COSTN_SMT_전수파싱.md, 26_편집기와_사이클식.md, 28_변환_수량산출_식언어.md, 34_화면구조_정리.md - */ -(function (global) { - "use strict"; - - const Calc = global.STMATE_CALC; - const fmt = Calc.fmt; - const num = Calc.num; - - const KIND_LABELS = { - ALL: "모든단가", - L: "노무비(L)", - M: "재료비(M)", - S: "경비(S)", - B: "일위대가(B)", - X: "중기비(X)", - D: "단가산출(D)", - W: "견적/표준시장(W)", - }; - - /** - * COSTN.SMT 텍스트를 실시간 평가하여 Q식, Cm식, 참조(&X00006L/M/S&) 단가 및 비목별 합계 산출 - */ - function evaluateSmtFormulaText(smtText, ctx, codeHint) { - if (global.STMATE_ENGINE && global.STMATE_ENGINE.evaluateSmtFormulaText) { - return global.STMATE_ENGINE.evaluateSmtFormulaText(smtText, ctx, codeHint); - } - return { vars: {}, linesOut: [], sumM: 0, sumL: 0, sumS: 0, sumT: 0 }; - } - - /** - * 1. wM_Set_Sys (초기값 입력 및 수정 화면) - */ - function renderWMSetSys(ctx) { - const sys = ctx.state.sysConfig; - const prj = (ctx.dbfData && ctx.dbfData.prjlst) || {}; - const evalRx = Calc.evalLeftToRight(sys.rxFormula); - - return ` -
-
- ⚙️ 초기값 입력 및 수정 [wM_Set_Sys · PRJLST.DBF 초기값 변수 42종] - 노임환산식 좌→우 평가: ${evalRx.toFixed(6)} -
-
- - 변경된 초기값은 기초단가·중기사용료·일위대가·내역서에 즉시 전파됩니다. -
- -
-
-
1. 환율 및 소수 자리수 설정 (EXLNM1/2 · KCD · ACD · ZCD · JCD · ECD · BPN)
-
-
-
-
-
-
-
-
-
- -
-
2. 노임 시간당 환산식 (RXNM_ · 좌→우 순차 평가 18번 §2.1) 및 특수노임 조건코드 (RX1~5CD)
-
-
-
-
-

- ※ STMATE 고유 파서 검증: 1/8*16/12*25/20 → 좌→우 순차 평가 0.20833333 (기계운전원 일급 267,360원 × 0.20833333 = 55,700원/hr 정확 재현) -

-
- -
-
3. 현재 프로젝트 PRJLST.DBF 원본 초기값 필드
-
-
-
-
-
-
-
-
-
- `; - } - - /** - * 2. wM_JabPum (단가목록표 화면 — COSTN.DBF 100% 전수 조회·편집) - */ - function renderWMJabPum(ctx) { - const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || []; - const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; - const filterKind = ctx.uiState.danFilter || "ALL"; - const kw = (ctx.uiState.danSearch || "").trim().toLowerCase(); - - // state.resources 수정값 동기화 - const resMap = {}; - (ctx.computed.resList || []).forEach((r) => { - if (!r.isSynthetic) resMap[r.code] = r; - }); - - const counts = { ALL: rawCostn.length, L: 0, M: 0, S: 0, B: 0, X: 0, D: 0, W: 0 }; - rawCostn.forEach((r) => { - const p = String(r.CODE || " ")[0].toUpperCase(); - if (counts[p] !== undefined) counts[p]++; - }); - - const filtered = rawCostn.filter((r) => { - const code = String(r.CODE || "").trim(); - if (!code) return false; - const p = code[0].toUpperCase(); - if (filterKind !== "ALL" && p !== filterKind) return false; - if (kw) { - const memoTxt = memos[code] ? memos[code].text : ""; - const hay = - `${code} ${r.NAME || ""} ${r.SIZE || ""} ${r.UNIT || ""} ${memoTxt}`.toLowerCase(); - if (!hay.includes(kw)) return false; - } - return true; - }); - - return ` -
-
- 📋 단가목록표 [wM_JabPum · COSTN.DBF 전수 원장] — 총 ${rawCostn.length}건 중 ${filtered.length}건 표시 - 5개 가격지 후보(AMT_11~51) 및 적용단가(TAMT1 = MAMT1 + LAMT1 + SAMT1) -
- -
- ${Object.keys(KIND_LABELS) - .map( - (k) => ` - - `, - ) - .join("")} - - - - -
- -
-
-
- - - - - - - - - - - - - - - - - - - - - ${filtered - .map((r, i) => { - const code = String(r.CODE || "").trim(); - const p = code[0].toUpperCase(); - const res = resMap[code]; - const m1 = res - ? res.kind === "재료비" - ? res.appliedPrice - : 0 - : num(r.MAMT1); - const l1 = res - ? res.kind === "노무비" - ? res.appliedPrice - : 0 - : num(r.LAMT1); - const s1 = res ? (res.kind === "경비" ? res.appliedPrice : 0) : num(r.SAMT1); - const t1 = m1 + l1 + s1 || num(r.TAMT1); - const hasMemo = Boolean(memos[code]); - return ` - - - - - - - - - - - - - - - - - `; - }) - .join("")} - -
No코드(CODE)구분명칭 (NAME)규격 (SIZE)단위재료단가(MAMT1)노무단가(LAMT1)경비단가(SAMT1)합계단가(TAMT1)1단가(조달)2단가(물가)3단가(거래)산근/대가
${i + 1}${code}${p}${r.NAME || ""}${r.SIZE || ""}${r.UNIT || ""} - - - - - - ${fmt(t1)}${num(r.AMT_11) ? fmt(r.AMT_11) : "-"}${num(r.AMT_21) ? fmt(r.AMT_21) : "-"}${num(r.AMT_31) ? fmt(r.AMT_31) : "-"} - ${ - hasMemo || p === "B" || p === "D" || p === "X" - ? `` - : "-" - } -
-
-
-
-
- `; - } - - /** - * 3. wM_Edit_iLWi / wM_Edit_San / wM_Edit_EQ (일위대가 · 단가산출근거 · 중기사용료 통합 편집 화면) - * - 좌측: COSTN.DBF 내 B(일위대가), D(단가산출), X(중기) 전체 목록 + COSTN.SMT 보유 여부 - * - 우측 상단: 선택 항목의 COSTN.SMT 원문 편집기 (`&X00006L&`, `Q=3600*q*K*f*E/Cm`, `:=A01`) - * - 우측 하단: 실시간 파싱·변수 평가 결과 테이블 및 [단가에 반영(F9)] - */ - function renderWMEditIlwi(ctx) { - const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || []; - const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; - const subFilter = ctx.uiState.ilwiSubFilter || "ALL_BDX"; - - const bdxRows = rawCostn.filter((r) => { - const c = String(r.CODE || "").trim(); - if (!c) return false; - const p = c[0].toUpperCase(); - if (subFilter === "ALL_BDX") return p === "B" || p === "D" || p === "X" || Boolean(memos[c]); - if (subFilter === "SMT_ONLY") return Boolean(memos[c]); - return p === subFilter; - }); - - const selCode = - ctx.uiState.selectedIlwiCode && - bdxRows.some((r) => String(r.CODE).trim() === ctx.uiState.selectedIlwiCode) - ? ctx.uiState.selectedIlwiCode - : bdxRows.find((r) => Boolean(memos[String(r.CODE || "").trim()])) || bdxRows[0] - ? String( - (bdxRows.find((r) => Boolean(memos[String(r.CODE || "").trim()])) || bdxRows[0]).CODE, - ).trim() - : ""; - - const selRow = bdxRows.find((r) => String(r.CODE).trim() === selCode) || {}; - const memoObj = memos[selCode] || null; - const recipeComps = - global.STMATE_ENGINE && global.STMATE_ENGINE.getRecipeBreakdown - ? global.STMATE_ENGINE.getRecipeBreakdown(ctx, selCode) - : []; - const defaultText = memoObj - ? memoObj.text - : recipeComps.length - ? `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""}) — 표준 호표 조립 구성 (${recipeComps.length}행)'\r\n` + - recipeComps - .map( - (c, ci) => - `'${ci + 1}. ${c["명칭"]} (${c["규격"] || "-"}) : 수량 ${c["수량"]} ${c["단위"] || ""}'`, - ) - .join("\r\n") + - `\r\n'노 무 비 : ${num(selRow.LAMT1)}'\r\n'재 료 비 : ${num(selRow.MAMT1)}'\r\n'경 비 : ${num(selRow.SAMT1)}'\r\n(==)` - : `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""})'\r\n'1. 재료비 : ${fmt(num(selRow.MAMT1))}원'\r\n'2. 노무비 : ${fmt(num(selRow.LAMT1))}원'\r\n'3. 경 비 : ${fmt(num(selRow.SAMT1))}원'\r\n(==)`; - const currentSmtText = - ctx.uiState.editedSmtMap && ctx.uiState.editedSmtMap[selCode] !== undefined - ? ctx.uiState.editedSmtMap[selCode] - : defaultText; - - const evalRes = evaluateSmtFormulaText(currentSmtText, ctx, selCode); - - return ` -
-
- 🧮 일위대가 · 단가산출근거 · 중기사용료 편집기 [wM_Edit_iLWi / wM_Edit_San / COSTN.SMT] - 선택 호표: ${selCode} ${selRow.NAME || ""} (SIxMemo 블록 #${memoObj ? memoObj.blockNo : "-"}, ${memoObj ? memoObj.byteLen : 0} bytes) -
- -
- - - - - - - -
- -
- -
-
- 호표 마스터 목록 (클릭 시 우측 COSTN.SMT 전개식 로드) - 총 ${bdxRows.length}건 -
-
- - - - - - - - - - - - - ${bdxRows - .map((r) => { - const c = String(r.CODE || "").trim(); - const isSel = c === selCode; - const hasM = Boolean(memos[c]); - return ` - - - - - - - - - `; - }) - .join("")} - -
코드품명 (NAME)규격단위합계단가SMT
${c}${r.NAME || ""}${r.SIZE || ""}${r.UNIT || ""}${fmt(num(r.TAMT1))}${hasM ? "📝" : "-"}
-
-
- - -
-
-
- 📝 COSTN.SMT 전개 텍스트 편집기 — &X00026L& 참조 · Q=3600*q*K*f*E/Cm · :=A01 문법 실시간 해석 - DB 저장단가: 재 ${fmt(num(selRow.MAMT1))} / 노 ${fmt(num(selRow.LAMT1))} / 경 ${fmt(num(selRow.SAMT1))} / 계 ${fmt(num(selRow.TAMT1))}원 -
- -
- -
-
- 🔍 SIxMemo 수식 파서 실시간 평가 결과 (변수: ${ - Object.keys(evalRes.vars) - .map((k) => `${k}=${evalRes.vars[k]}`) - .join(", ") || "없음" - }) - 평가 합계: 재 ${fmt(evalRes.sumM)} + 노 ${fmt(evalRes.sumL)} + 경 ${fmt(evalRes.sumS)} = ${fmt(evalRes.sumT)}원 -
-
- - - - - - - - - - - - - ${evalRes.linesOut - .map( - (ln) => ` - - - - - - - - - `, - ) - .join("")} - -
행산식 원문 (COSTN.SMT Line)변수 대입/평가자원참조(&코드&)참조단가산출금액
${ln.lineNo}${ln.raw}${ln.assigned || ""}${ln.refInfo ? `${ln.refInfo.refCode}[${ln.refInfo.compTag}] (${ln.refInfo.refName})` : ""}${ln.refInfo ? fmt(ln.refInfo.baseUnitPrice) : ""}${ln.refInfo ? fmt(ln.refInfo.calcAmt, 1) : ""}
-
-
-
-
-
- `; - } - - /** - * 4. 단가/카탈로그 모달 생성기 (wM_Boxa / wM_Edt_Hal / 표준카탈로그 894코드·350레시피·21특수산식) - */ - function buildDanModalHtml(modalKind, ctx) { - if (modalKind === "wM_Boxa") { - return ` -
-

[wM_Boxa] 5개 가격지 단가 비교·일괄 채택 — 자재 후보단가(AMT_11~51) 중 지정 슬롯 또는 최저가를 적용단가(AMT_61/MAMT1)로 일괄 채택합니다.

-
-
- -
-
-
-
- `; - } - if (modalKind === "wM_Edt_Hal") { - return ` -
-

[wM_Edt_Hal] 기준단가 비율 일괄 수정 — 적용단가 = 기준단가 × 비율(%) (적용단가 0원 및 보호항목 UC=T 제외).

-
-
-
-
-
-
-
- `; - } - // 표준 카탈로그 (894 코드 · 350 일위대가 레시피 · 21 특수 단가산출) - const cat = ctx.catalogData || { codes: [], recipes: [], special_san: [] }; - return ` -
-

- [STMATE 표준 카탈로그 브라우저] — 30_원자료에서 추출한 표준코드 ${cat.codes.length}종 · 일위대가 레시피 ${cat.recipes.length}종 · 특수 단가산출 ${cat.special_san.length}종 -

-
-
-
특수 단가산출 21개 (Q식/사이클식 원문)
-
- - - - ${(cat.special_san || []) - .map( - (s, idx) => ` - - - - - - - - `, - ) - .join("")} - -
호표명칭단위Q식편집기로 복사
#${s["호표"]}${s["명칭"]}${s["단위"]}${(s["인자"] && s["인자"].Q) || ""}
-
-
-
-
표준 일위대가 레시피 (${cat.recipes.length}종 중 상위 60종)
-
- - - - ${(cat.recipes || []) - .slice(0, 60) - .map( - (r) => ` - - - - - - - - `, - ) - .join("")} - -
구분명칭규격단위구성수
${r["종류"]}${r["명칭"]}${r["규격"] || ""}${r["단위"] || ""}${(r["구성"] || []).length}행
-
-
-
-
- `; - } - - global.STMATE_VIEW_DAN_ILWI = { - evaluateSmtFormulaText, - renderWMSetSys, - renderWMJabPum, - renderWMEditIlwi, - buildDanModalHtml, - }; -})(window); diff --git a/tools/stmate-test/view_kanjub_prt.js b/tools/stmate-test/view_kanjub_prt.js deleted file mode 100644 index 9b803e6a3..000000000 --- a/tools/stmate-test/view_kanjub_prt.js +++ /dev/null @@ -1,647 +0,0 @@ -/** - * STMATE w 8.0 — 제잡비 계산(wM_KanJub) · 원가계산기준 제비율표(wM_KAN_RATE) · 계약/실행/기성(wM_Mk_Cont · wM_KanJub_K) · 보고서 인쇄/변환(PM_Prt 30종) - * 근거: 30_원자료/TWM_KANJUB.json, TWM_KAN_RATE.json, 22번 §5~6, 27번 계약·실행·기성, 28번 XML/JDL, 34번 30종 출력 시트 - */ -(function (global) { - "use strict"; - - const Calc = global.STMATE_CALC; - const fmt = Calc.fmt; - const num = Calc.num; - - /** - * 1. TWM_KANJUB (wM_KanJub 제잡비 계산 및 원가계산서 화면) - * - 상단 바: 설계차수(원설계/1~3차변경), 방식(원가계산/표준시장), 간접비 절사단위, 기준선택(JB_R), 요율수정, 결과보기 - * - 중단 Pn0_Input: 16개 제잡비 설정 콤보/입력칸 - * - 하단 Pg_Ctrl 4탭: 일반형식(T) | 수공형식(S) | 실적일반(TA) | 실적수공(SA) 원가계산서 그리드 - */ - function renderWMKanJub(ctx) { - const kc = ctx.state.kanjubConfig; - const kj = ctx.computed.kanjubComputed; - const smt = (ctx.dbfData && ctx.dbfData.prjlst_smt) || {}; - const jbrFiles = (ctx.sourcesData && ctx.sourcesData.jbr_files) || []; - - return ` -
-
- 🏛️ 제잡비 계산 및 원가계산서 [TWM_KANJUB · PRJLST.SMT ${Object.keys(smt).length}개 키 연동] - 직접공사비: ${fmt(kj.DIRECT_COST)}원 → 도급액: ${fmt(kj.FINAL_SUPPLY)}원 + 관급자재: ${fmt(kj.GOV_MAT)}원 = 총공사비: ${fmt(kj.GRAND_TOTAL)}원 -
- - -
- - - - - - - - - -
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
- 원가계산서 양식 탭 (Pg_Ctrl): - ${[ - { id: "T", label: "탭1: 일반형식 (T · 재+노+경)" }, - { id: "S", label: "탭2: 수공형식 (S · 경비 16세부비목)" }, - { id: "TA", label: "탭3: 실적일반 (TA · 표준시장단가 병행)" }, - { id: "SA", label: "탭4: 실적수공 (SA · 실적+수공)" }, - ] - .map( - (t) => ` - - `, - ) - .join("")} - - 이윤 끝전조정: ${fmt(kj.PROFIT_ADJ)}원 (초기 이윤 ${fmt(kj.IYUN_INIT)}원 → 최종 이윤 ${fmt(kj.IYUN)}원) - -
- -
-
-
- - - - - - - - - - - - - - ${kj.lines - .map((ln, idx) => { - const isRateEditable = kc.rates[ln.key] !== undefined; - const isStrong = - ln.key === "DIRECT_COST" || - ln.key === "TOTAL_COST" || - ln.key === "FINAL_SUPPLY" || - ln.key === "GRAND_TOTAL"; - return ` - - - - - - - - - - `; - }) - .join("")} - -
순번비목코드원가계산서 항목명산정기초 공식 및 밑수 설명 (REA 03·04·16번 문서 검증)산정기초 금액(원)적용요율(%)산출금액(원)
${idx + 1}${ln.key}${ln.label}${ln.formula}${ln.base !== null && ln.base !== undefined ? fmt(ln.base) : "-"} - ${ - isRateEditable - ? `` - : ln.rate !== null && ln.rate !== undefined - ? fmt(ln.rate, 3) + "%" - : "-" - } - ${fmt(ln.amount)}
-
-
-
-
- `; - } - - /** - * 2. TWM_KAN_RATE (wM_KAN_RATE 원가계산기준 제비율표 4탭 편집 화면) - * - Tab_Rn: 일반 제잡비율 (RATE_R.DBF 50행 × 35열) - * - Tab_RA: 실적공사 제잡비율 (RATE_RA.DBF 50행 × 20열) - * - Tab_M: 일반관리비율 (RATE_M.DBF 25행 × 10열) - * - Tab_B: 보증수수료율 (RATE.DBF / RATE_B) - */ - function renderWMKanRate(ctx) { - const rateTables = (ctx.dbfData && ctx.dbfData.rate_tables) || {}; - const activeTab = ctx.uiState.kanRateTab || "RATE_R.DBF"; - const rows = rateTables[activeTab] || []; - const colKeys = rows[0] ? Object.keys(rows[0]).filter((k) => !k.startsWith("_")) : []; - const jbrFiles = (ctx.sourcesData && ctx.sourcesData.jbr_files) || []; - - const ROW_MEANINGS = { - 0: "공사규모 기준액 구간 헤더", - 6: "간접노무비(1~5열) · 기타경비(6~10열) — 5천만 미만", - 7: "간접노무비 · 기타경비 — 5천만~3억 미만", - 8: "간접노무비 · 기타경비 — 3억~10억 미만", - 9: "간접노무비 · 기타경비 — 10억~30억 미만 (중규모)", - 20: "선택 적용 행: 간접노무(1) · 기타경비(6) · 일반관리비(11) · 이윤(16) · 산안비(17) · 기초액(18)", - 45: "산재보험료율(1열) · 고용보험료율(17~20열 등급별)", - 46: "건강보험(1열) · 국민연금(6열) · 노인장기요양(11열)", - }; - - return ` -
-
- 📊 원가계산기준 제비율표 조회·수정 [TWM_KAN_RATE · ${activeTab} (${rows.length}행 × ${colKeys.length}열)] - 행20(선택적용) · 행45(산재/고용) · 행46(건보/연금/요양) 좌표 매핑 검증 완료 -
- -
- ${[ - { id: "RATE_R.DBF", label: "탭1(Tab_Rn): 일반 제잡비율 (RATE_R 50×35)" }, - { id: "RATE_RA.DBF", label: "탭2(Tab_RA): 실적공사 제잡비율 (RATE_RA 50×20)" }, - { id: "RATE_M.DBF", label: "탭3(Tab_M): 일반관리비율 (RATE_M 25×10)" }, - { id: "RATE.DBF", label: "탭4(Tab_B): 보증/기타요율 (RATE)" }, - ] - .map( - (t) => ` - - `, - ) - .join("")} - - -
- -
-
-
- - - - - - ${colKeys.map((ck) => ``).join("")} - - - - ${rows - .map((r, rIdx) => { - const highlight = ROW_MEANINGS[rIdx] ? "boq-row-T" : ""; - return ` - - - - ${colKeys.map((ck) => ``).join("")} - - `; - }) - .join("")} - -
행(0~)STMATE 행 좌표 의미${ck}
#${rIdx}${ROW_MEANINGS[rIdx] || `구간/공사종류 데이터 행 #${rIdx}`}${r[ck]}
-
-
-
-
- `; - } - - /** - * 3. 계약 / 실행예산 / 기성 / 설계변경 단계 통합 화면 (wM_Mk_Cont · wM_Boq_ExecX · wM_KanJub_K · 설계변경) - * 근거: 27_계약_실행_기성_단계.md - */ - function renderWStage(ctx) { - const stageSub = ctx.uiState.stageSubTab || "CONTRACT"; - const bidRateM = num(ctx.uiState.bidRateM || 87.745); - const bidRateL = num(ctx.uiState.bidRateL || 87.745); - const bidRateS = num(ctx.uiState.bidRateS || 87.745); - const progPct = num(ctx.uiState.progPct || 65.0); - const prevPct = num(ctx.uiState.prevPct || 30.0); - - const comp = ctx.computed; - const leafRows = comp.boqComputed.rows || []; - const revComp = comp.revComputed; - - return ` -
-
- 🔄 설계 후속 단계 관리 [27번 문서 — 계약내역(wM_Mk_Cont) · 실행예산(wM_Boq_ExecX) · 기성내역(wM_KanJub_K) · 설계변경 대비] - K_ORD 단계 전이: 설계(0) → 계약(1) → 변경(2) → 실행(3) → 기성(4) -
- -
- - - - -
- - ${ - stageSub === "CONTRACT" - ? ` -
-
-
-
-
-
-
-
-
- 계약내역서 (설계단가 × 낙찰율 → 계약단가 및 계약금액 산출 · 적용제외 공정 보호 지원) - 설계 직공비: ${fmt(comp.boqComputed.totals.directT)}원 → 계약 직공비: ${fmt(Math.floor((comp.boqComputed.totals.directT * bidRateM) / 100))}원 -
-
- - - - - - ${leafRows - .map((r) => { - const cM = Math.floor((r.mUnit * bidRateM) / 100); - const cL = Math.floor((r.lUnit * bidRateL) / 100); - const cS = Math.floor((r.sUnit * bidRateS) / 100); - const cT = cM + cL + cS; - const cAmt = Math.floor(r.qty * cT); - return ` - - - - - - - - - - - - `; - }) - .join("")} - -
코드공종명규격단위수량설계단가설계금액계약단가(낙찰적용)계약금액
${r.code}${r.name}${r.spec}${r.unit}${fmt(r.qty, 2)}${fmt(r.tUnit)}${fmt(r.tAmt)}${fmt(cT)}${fmt(cAmt)}
-
-
-
` - : "" - } - - ${ - stageSub === "EXEC" - ? ` -
-
-
[wM_Boq_ExecX] 실행예산 내역서 (계약금액 대비 현장 실행예산율 92.0% 기준 비교)
-
- - - - ${leafRows - .map((r) => { - const exUnit = Math.floor(r.tUnit * 0.92); - const exAmt = Math.floor(r.qty * exUnit); - return ` - - - - - - - - - - `; - }) - .join("")} - -
코드공종명수량설계금액실행단가(92%)실행예산금액절감액(차액)
${r.code}${r.name}${fmt(r.qty, 2)}${fmt(r.tAmt)}${fmt(exUnit)}${fmt(exAmt)}${fmt(r.tAmt - exAmt)}
-
-
-
` - : "" - } - - ${ - stageSub === "GISUNG" - ? ` -
-
-
-
-
-
-
-
- [wM_KanJub_K] 기성내역 및 기성 제잡비 계산 (금회직공비 × 계약제잡비율 · 계약 | 전회 | 금회 | 누계 | 잔여) - 금회 기성율: ${(progPct - prevPct).toFixed(1)}% | 누계 기성도급액: ${fmt(Math.floor((comp.kanjubComputed.FINAL_SUPPLY * progPct) / 100))}원 -
-
- - - - - - ${leafRows - .map((r) => { - const prevA = Math.floor((r.tAmt * prevPct) / 100); - const cumA = Math.floor((r.tAmt * progPct) / 100); - const curA = cumA - prevA; - const curQ = (r.qty * (progPct - prevPct)) / 100; - return ` - - - - - - - - - - - - `; - }) - .join("")} - -
코드공종명계약수량계약금액전회기성액(${prevPct}%)금회기성수량금회기성액(${(progPct - prevPct).toFixed(1)}%)누계기성액(${progPct}%)잔여금액
${r.code}${r.name}${fmt(r.qty, 2)}${fmt(r.tAmt)}${fmt(prevA)}${fmt(curQ, 2)}${fmt(curA)}${fmt(cumA)}${fmt(r.tAmt - cumA)}
-
-
-
` - : "" - } - - ${ - stageSub === "REVISION" - ? ` -
-
-
- 설계변경 원가계산 대비표 (당초 도급액: ${fmt(comp.kanjubComputed.FINAL_SUPPLY)}원 vs 변경 도급액: ${fmt(revComp.revKj.FINAL_SUPPLY)}원 · 사후정산 건보/요양/연금 당초고정 지원) -
-
- - - - - - ${leafRows - .map( - (r) => ` - - - - - - - - - - - `, - ) - .join("")} - -
코드공종명당초수량(QTY1)변경수량(QTY2)단가당초금액변경금액증감액(차액)
${r.code}${r.name}${fmt(r.qty, 2)}${fmt(r.tUnit)}${fmt(r.tAmt)}${fmt(r.changedTAmt)}${fmt(r.diffTAmt)}
-
-
-
` - : "" - } -
- `; - } - - /** - * 4. PM_Prt (ST_Prt.CPM 30종 보고서 인쇄 미리보기 · SET_REP.DBF 75종 양식 · 엑셀/조달청 XML/JDL 내보내기) - */ - function renderPMPrt(ctx) { - const activeRep = ctx.uiState.activeReportSheet || "0608"; - const setRepRows = ((ctx.dbfData && ctx.dbfData.set_rep_rows) || []).filter( - (r) => r.NAME || r.HEADTITLE, - ); - const kj = ctx.computed.kanjubComputed; - const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx); - const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || []; - const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; - - return ` -
-
- 🖨️ 보고서 인쇄 및 엑셀/조달청 XML/JDL 변환 [PM_Prt · ST_Prt.CPM 30종 시트 · SET_REP.DBF ${setRepRows.length}종 양식] - 인쇄 옵션: 굴림체 · 비고=코드/호표 · 수량/단위 표준 · 일반양식 -
- -
- - - - - - - -
- -
-
-

- ${ - { - "0608": "공 사 원 가 계 산 서", - "0601": "총 괄 설 계 내 역 서", - "0603": "설 계 내 역 서", - "0607": "관 급 자 재 내 역 서", - "0643": "설 계 변 경 내 역 서 (증 감 대 비)", - "0623": "계 약 내 역 서 · 실 행 예 산 서", - "0633": "기 성 부 분 내 역 서 · 수 량 조 서", - "0302": "일 위 대 가 표 (목 록)", - "0402": "단 가 산 출 근 거", - "0203": "시 간 당 중 기 사 용 료", - "0101": "자 재 · 노 무 · 경 비 단 가 목 록 표", - "0501": "자 원 · 중 기 · 대 가 수 량 금 액 집 계 표", - "0201": "환 율 및 기 초 자 료 (〓 INITIAL 〓)", - SET_REP_ALL: "SET_REP.DBF 75종 양식 설정표", - }[activeRep] || "설 계 보 고 서" - } -

-

- 공사명: ${ctx.state.projectMeta.pname} (총공사비: ${fmt(kj.GRAND_TOTAL)}원 / 도급액: ${fmt(kj.FINAL_SUPPLY)}원) -

- - ${ - activeRep === "0608" - ? ` - - - ${kj.lines - .map( - (ln) => ` - - - - - - - `, - ) - .join("")} - -
비목산정기초(원)요율(%)금액(원)산출근거
${ln.label}${ln.base ? fmt(ln.base) : ""}${ln.rate !== null && ln.rate !== undefined ? fmt(ln.rate, 3) + "%" : ""}${fmt(ln.amount)}${ln.formula}
` - : "" - } - - ${ - ["0601", "0603", "0607", "0643", "0623", "0633"].includes(activeRep) - ? ` - - - ${boqRows - .filter((r) => - activeRep === "0607" - ? r._isExcl - : activeRep === "0601" - ? /^(T\d|E\d|C\d)$/.test(String(r.BDCODE1 || "").trim()) - : true, - ) - .map( - (r) => ` - - - - - - - - - - - - - `, - ) - .join("")} - -
공종명칭규격단위수량재료비단가노무비단가경비단가합계단가합계금액비고
${r.ITNUM || ""}${r.NAME1 || ""}${r.SIZE1 || ""}${r.UNIT1 || ""}${r.QTY1 ? fmt(r.QTY1, 2) : ""}${r.MAMT1 ? fmt(r.MAMT1) : ""}${r.LAMT1 ? fmt(r.LAMT1) : ""}${r.SAMT1 ? fmt(r.SAMT1) : ""}${r.TAMT1 ? fmt(r.TAMT1) : ""}${r.T_AMT1 ? fmt(r.T_AMT1) : ""}${r.BDCODE1 || ""}
` - : "" - } - - ${ - activeRep === "0402" - ? Object.values(memos) - .map( - (m) => ` -
-
[${m.code}] ${(costnRows.find((x) => x.CODE === m.code) || {}).NAME || ""}
-
${m.text}
-
`, - ) - .join("") - : "" - } - - ${ - ["0302", "0203", "0101", "0501", "0201"].includes(activeRep) - ? ` - - - ${costnRows - .filter((r) => { - const p = String(r.CODE || " ")[0].toUpperCase(); - if (activeRep === "0302") return p === "B" || p === "D"; - if (activeRep === "0203") return p === "X"; - if (activeRep === "0201") return p === "L" || p === "X"; - return p === "L" || p === "M" || p === "S"; - }) - .map( - (r) => ` - - - - - - - - - - `, - ) - .join("")} - -
코드품명규격단위재료비노무비경비합계단가
${r.CODE}${r.NAME || ""}${r.SIZE || ""}${r.UNIT || ""}${fmt(num(r.MAMT1))}${fmt(num(r.LAMT1))}${fmt(num(r.SAMT1))}${fmt(num(r.TAMT1))}
` - : "" - } - - ${ - activeRep === "SET_REP_ALL" - ? ` - - - ${setRepRows - .map( - (r) => ` - - - - - - - - - - `, - ) - .join("")} - -
양식코드(INDX)보고서명(NAME)인쇄 표제(HEADTITLE)컬럼수(FLDCNT)필드1필드2필드3필드4
${r.INDX}${r.NAME}${r.HEADTITLE}${r.FLDCNT}${r.FLD_N1 || ""}${r.FLD_N2 || ""}${r.FLD_N3 || ""}${r.FLD_N4 || ""}
` - : "" - } -
-
-
- `; - } - - global.STMATE_VIEW_KANJUB_PRT = { - renderWMKanJub, - renderWMKanRate, - renderWStage, - renderPMPrt, - }; -})(window); diff --git a/tools/stmate-test/view_prj_boq.js b/tools/stmate-test/view_prj_boq.js index 9b71bfe5f..e93ee5dc6 100644 --- a/tools/stmate-test/view_prj_boq.js +++ b/tools/stmate-test/view_prj_boq.js @@ -1,31 +1,13 @@ /** - * STMATE w 8.0 — 공사목록표(wPrjList) · 계층 설계내역서(wBoq) · 내역도구 모달(wITNum/wBoqRate/wM_Rate/wExpnd/wQtyList) - * 근거: REA 09_보조분석 resource-TWPRJLIST.dfm.json, 16번 §4.2, 22번 §4, 28번 §2.2, 34번 §2.1 + * STMATE w 8.0 — 계층 내역 재집계(evaluateFullBoqRows) · 단가 반영(syncCostnUnitPrice) · 저장 요청 묶음 + * 근거: 16번 §4.2, 22번 §4, 28번 §2.2, 34번 §2.1 */ (function (global) { "use strict"; const Calc = global.STMATE_CALC; - const fmt = Calc.fmt; const num = Calc.num; - const PCLASS_NAMES = { - 0: "일반토목/산림토목", - 1: "건축공사", - 2: "조경공사", - 3: "산업설비", - 4: "전기/통신", - 7: "전문/설계변경", - }; - - const KORD_BADGES = { - 0: "설계내역(0)", - 1: "계약내역(1)", - 2: "1차변경(2)", - 3: "실행예산(3)", - 4: "기성내역(4)", - }; - function isStructCode(bd) { return /^(T\d|E\d|C\d|:)$/.test(String(bd || "").trim()); } @@ -144,381 +126,6 @@ return rows; } - /** - * 1. TWPRJLIST (wPrjList 공사목록표 화면) - * 상단: 등록정보 수정 | 제잡비 계산 | 해당분 수정(wBoq) | 복사등록 | 해당분만 압축(.STC) | 내역서 미리보기 | 정렬 - * 중단: RGrdPrjList (PC 내 9개 프로젝트 + 현재 열린 PRJLST.DBF) - * 하단: RDbGrid_Boq (선택 프로젝트의 실시간 내역서 미리보기) - */ - function renderWPrjList(ctx) { - const sources = (ctx.sourcesData && ctx.sourcesData.projects) || []; - const curPath = ctx.currentSourcePath || ""; - const prj = (ctx.dbfData && ctx.dbfData.prjlst) || {}; - const comp = ctx.computed; - const directCost = comp.boqComputed.totals.directT; - const totalCost = comp.kanjubComputed.GRAND_TOTAL; - const boqRows = evaluateFullBoqRows(ctx); - const showPreview = ctx.uiState.showBoqPreview !== false; - - return ` -
-
- 📁 공사목록표 [TWPRJLIST · PRJLST.DBF / PRJLST.SMT] — 총 ${sources.length}개 프로젝트 연결됨 - 더블클릭 시 설계내역서(wBoq) 진입 -
- - -
- - - - - - - - - - - 현재 선택: ${prj.PNAME || ctx.state.projectMeta.pname} (직공비 ${fmt(directCost)}원 / 총공사비 ${fmt(totalCost)}원) - -
- -
- -
-
- RGrdPrjList — PC 내 STMATE 프로젝트 원장 (클릭: 프로젝트 로드 / 더블클릭: 내역서 편집) - K_ORD 단계: ${KORD_BADGES[num(prj.K_ORD)] || "설계내역(0)"} -
-
- - - - - - - - - - - - - - - - - ${sources - .map((src, idx) => { - const isCur = src.path === curPath; - const pnum = isCur ? prj.PNUM || "2024년 단가" : "2024~2025"; - const pcode = isCur ? prj.PCODE || "11" : "11"; - const pcls = isCur - ? PCLASS_NAMES[num(prj.PCLASS)] || "일반토목" - : "산림/임도토목"; - const kord = isCur ? KORD_BADGES[num(prj.K_ORD)] || "설계(0)" : "설계(0)"; - const tcost = isCur ? fmt(directCost) : "로드 시 표시"; - const costw = isCur ? fmt(totalCost) : "로드 시 표시"; - return ` - - - - - - - - - - - - - `; - }) - .join("")} - -
순번구분공사번호(PNUM)공사명 (PNAME / 아카이브명)코드(PCODE)공사종류(PCLASS)단계(K_ORD)직공비(TCOST1)총공사비(COSTW1)작업
${idx + 1}${src.kind.toUpperCase()}${pnum}${src.name}BOQ${pcode}${pcls}${kord}${tcost}${costw} - -
-
-
- - ${renderPrjRows(ctx)} - - ${ - showPreview - ? ` - -
-
- RDbGrid_Boq — 내역서 미리보기 (${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"} · 총 ${boqRows.length}행 전수 표시) - 공종(ITNUM) | 명칭(NAME1) | 규격(SIZE1) | 수량(QTY1) | 단위(UNIT1) | 단가(TAMT1) | 금액(T_AMT1) | 비고(BDCODE1) -
-
- - - - - - - - - - - - - - - - ${boqRows - .map( - (r, i) => ` - - - - - - - - - - - - `, - ) - .join("")} - -
행번공종(ITNUM)명칭 (NAME1)규격 (SIZE1)수량 (QTY1)단위합계단가 (TAMT1)합계금액 (T_AMT1)비고 (BDCODE1)
${i + 1}${r.ITNUM || ""}${r.NAME1 || ""}${r.SIZE1 || ""}${r.QTY1 ? fmt(r.QTY1, 2) : ""}${r.UNIT1 || ""}${r.TAMT1 ? fmt(r.TAMT1) : ""}${r.T_AMT1 ? fmt(r.T_AMT1) : ""}${r.BDCODE1 || ""}
-
-
` - : "" - } -
-
- `; - } - - // 열린 프로젝트의 공사(PRJLST 행 · PCODE 1n ↔ BOQ1n) — 복사등록·해당분 압축 대상 고름(scr_file.js) - function renderPrjRows(ctx) { - const rows = (ctx.dbfData && ctx.dbfData.prjlst_all) || []; - if (!rows.length) return ""; - const pick = ctx.uiState.filePcode || String(rows[0].PCODE || "11").trim(); - const tr = (r) => { - const pc = String(r.PCODE || "").trim(); - return `BOQ${pc}${r.PNAME || ""}${KORD_BADGES[num(r.K_ORD)] || r.K_ORD || ""}${fmt(num(r.TCOST1))}${fmt(num(r.TCOST2))}${fmt(num(r.COSTW1))}`; - }; - return `
공사 (PRJLST ${rows.length}행) — 고른 공사로 복사등록·해당분 압축
${rows.map(tr).join("")}
고름표공사명단계(K_ORD)직공비 1차직공비 2차총공사비 1차
`; - } - - /** - * 2. wBoq (계층 설계내역서 편집 화면) - * - BOQ11~14.DBF의 모든 행(T1~T6 공종제목, E1~E6 소계/계/총계/총합계, C* 제외구역, 말단 공종행) 100% 표시 - * - 레벨 필터(전체 / = 소계 이상 / == 계 이상 / ==== 총계 이상 / 제외구역만) - * - 공종번호 자동부여(wITNum), 할증율(wBoqRate), 단수보정(wM_Rate), 일위대가 풀기(wExpnd), 보관함(1)(2)(3) - */ - function renderWBoq(ctx) { - const allRows = evaluateFullBoqRows(ctx); - const lvFilter = ctx.uiState.boqLevelFilter || "ALL"; - const kw = (ctx.uiState.boqSearch || "").trim().toLowerCase(); - const pocketIdx = ctx.uiState.activePocket || 1; - const pockets = ctx.uiState.boqPockets || { 1: [], 2: [], 3: [] }; - - const filtered = allRows.filter((r) => { - const bd = String(r.BDCODE1 || "").trim(); - if (lvFilter === "STRUCT" && !isStructCode(bd)) return false; - if (lvFilter === "LEAF" && (isStructCode(bd) || !bd)) return false; - if (lvFilter === "EXCL" && !r._isExcl) return false; - if (kw) { - const txt = `${r.ITNUM || ""} ${r.NAME1 || ""} ${r.SIZE1 || ""} ${bd}`.toLowerCase(); - if (!txt.includes(kw)) return false; - } - return true; - }); - - const totals = ctx.computed.boqComputed.totals; - - return ` -
-
- 📐 설계내역서 편집 [wBoq · ${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"}] — 전체 ${allRows.length}행 (표시 ${filtered.length}행) - 직공비 합계: ${fmt(totals.directT)}원 | 관급·제외구역(C*): ${fmt(totals.govMatTotal + totals.govFeeTotal)}원 -
- - -
- - - - - - - - - -
- -
- -
-
- 계층 설계내역서 원장 (수량 수정 시 비목별 ROUNDDOWN 및 T*/E*/C* 집계 즉시 재계산 · 호표코드 더블클릭 시 대가표 이동) - 차액보정: ${ctx.state.sysConfig.chnMode === "total" ? "총액우선(OPTNM=" + ctx.state.sysConfig.optOrder + ")" : "비목우선"} -
-
- - - - - - - - - - - - - - - - - - - - - - - ${filtered - .map((r) => { - const bd = String(r.BDCODE1 || "").trim(); - const isLeaf = bd && !isStructCode(bd); - return ` - - - - - - - - - - - - - - - - - - - `; - }) - .join("")} - -
No공종(ITNUM)코드(BDCODE1)명칭 (NAME1)규격 (SIZE1)단위수량(QTY1)재료비단가노무비단가경비단가합계단가재료비금액노무비금액경비금액합계금액(T_AMT1)보관함
${r._idx + 1} - - - ${ - isLeaf - ? `${bd}` - : `${bd}` - } - - - ${r.SIZE1 || ""}${r.UNIT1 || ""} - ${ - isLeaf - ? `` - : "" - } - ${r.MAMT1 ? fmt(r.MAMT1) : ""}${r.LAMT1 ? fmt(r.LAMT1) : ""}${r.SAMT1 ? fmt(r.SAMT1) : ""}${r.TAMT1 ? fmt(r.TAMT1) : ""}${r.M_AMT1 ? fmt(r.M_AMT1) : ""}${r.L_AMT1 ? fmt(r.L_AMT1) : ""}${r.S_AMT1 ? fmt(r.S_AMT1) : ""}${r.T_AMT1 ? fmt(r.T_AMT1) : ""} - ${ - isLeaf - ? `` - : "" - } -
-
-
- - -
-
- 📦 내역보관함 (${pocketIdx}) -
- ${[1, 2, 3] - .map( - (n) => - ``, - ) - .join(" ")} -
-
-
-

- 자주 쓰는 공종을 보관함 (1)~(3)에 담아두고 현재 내역서에 즉시 삽입합니다. -

- ${ - (pockets[pocketIdx] || []).length === 0 - ? `
보관함 (${pocketIdx})이 비어 있습니다.
내역 행 우측 [담기]를 클릭하세요.
` - : ` - - - ${(pockets[pocketIdx] || []) - .map( - (p, pi) => ` - - - - - - - `, - ) - .join("")} - -
코드품명수량삽입
${p.code}${p.name}${fmt(p.qty, 2)}
` - } -
-
-
-
- `; - } - - /** - * 3. 내역 도구 모달 HTML 생성기 (wITNum / wBoqRate / wM_Rate / wExpnd / wQtyList) - */ - function buildBoqToolModalHtml(toolKind, ctx) { - if (toolKind === "wITNum") { - return `

[wITNum] 내역서 공종번호 자동입력 — 대공종(T4)/중공종(T2)/말단 공종행에 번호체계를 일괄 부여합니다.

`; - } - if (toolKind === "wBoqRate") { - return `

[wBoqRate] 내역 항목 할증율 일괄 적용 — 전체 또는 비목별(재·노·경) 할증 비율(%)을 적용합니다.

`; - } - if (toolKind === "wM_Rate") { - return `

[wM_Rate] 선택 범위 수량·비목 단수 일괄 보정 — 절사/반올림 및 소수점 자리수(0~4위)를 일괄 보정합니다.

`; - } - if (toolKind === "wExpnd") { - const bdxList = (ctx.state.ilwiItems || []).slice(0, 30); - return `

[wExpnd / wM_Expnd] 일위대가·단가산출 내역서 풀어넣기 — 선택한 호표의 재료·노무·경비 구성행을 내역서에 직접 전개합니다.

`; - } - const leafRows = ctx.computed.boqComputed.rows || []; - return `

[wQtyList] 수량산출서(QCODE.DBF) 코드별 수량 연결·집계 — 수량산출식 결과를 내역서 QTY1에 즉시 반영합니다.

${leafRows - .slice(0, 25) - .map( - (r) => - ``, - ) - .join("")}
코드공종명현재 내역수량산출근거 수식 (예: 3.46*1000)반영
${r.code}${r.name}${fmt(r.qty, 2)}
`; - } - function syncCostnUnitPrice(ctx, code, mVal, lVal, sVal) { const tVal = mVal + lVal + sVal; if (ctx.dbfData && ctx.dbfData.costn_rows) { @@ -576,9 +183,6 @@ global.STMATE_VIEW_PRJ_BOQ = { evaluateFullBoqRows, - renderWPrjList, - renderWBoq, - buildBoqToolModalHtml, syncCostnUnitPrice, buildSavePayload, };