refactor(stmate-test): 원본에 없는 기능 걷음 — 제비율표 JB 대조 보기 · 보고서 CSV · 계약 저장 공사 대조 · 옛 화면 9개(표준카탈로그·레시피·REA 아키텍처 창·가짜 JDL/XML)
- 원본 대조 = 설치본 실행파일 52개 화면 이름·버튼 글·한글 문자열 - 지움: conv_jb.py · excel_builder.py · view_dan_ilwi.js · view_kanjub_prt.js · /api/conv/jb-* · /api/report/csv · /api/stage/projects · /api/catalog · /api/export-xlsx - 낡은 시험 삭제(지운 기능 전용): conv_change 제비율표 3 · stage_api 공사별 설정 2 · report_sheets CSV 2줄 - 장부 F4-15 · F1-33 · F6-24 설명 고침 · stmate 시험 786 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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개 셀만 다름」
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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": "설계변경증감대비표",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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`(`<HII`: `tag=0x0008`, `byte_len`, `block_no * 64`) 기반 `COSTN.SMT` 산출근거 본문 추출·재기록기, `30_원자료` 카탈로그(894 표준코드·350 일위대가 레시피·21 특수산식) 로더 |
|
||||
| [`server.py`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/server.py) | 649 | FastAPI 서버 (`127.0.0.1:8095`) — `/api/sources`, `/api/catalog`, `/api/load`(COSTN·COSTN.SMT·BOQ11~14·PRJLST·RATE_R/RA/M·SET_REP 전수), `/api/load-jbr`, `/api/save` |
|
||||
| [`smt_parser.py`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/smt_parser.py) | 375 | dBASE III/IV(`DBF`) 파서·빌더, `COSTN.DBF` 10바이트 바이너리 `IFORM`(`<HII`: `tag=0x0008`, `byte_len`, `block_no * 64`) 기반 `COSTN.SMT` 산출근거 본문 추출·재기록기 |
|
||||
| [`server.py`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/server.py) | 649 | FastAPI 서버 (`127.0.0.1:8095`) — `/api/sources`, `/api/load`(COSTN·COSTN.SMT·BOQ11~14·PRJLST·RATE_R/RA/M·SET_REP 전수), `/api/load-jbr`, `/api/save` |
|
||||
| [`calc.js`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/calc.js) | 674 | 5단계 적산 파이프라인(`M/L/S → X 중기 → D 단가산출 → B 일위대가 → BOQ 내역서 → 원가계산서`) — 원가계산서는 실무 STC 4건 1원 대조 시험(`resources/tester/test_stmate_kanjub_stc.py`) |
|
||||
| [`view_prj_boq.js`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/view_prj_boq.js) | 677 | `wPrjList`(`TWPRJLIST` 공사목록표 + 하단 내역 미리보기), `wBoq`(`BOQ11~14.DBF` `T1~T6` 공종헤더·`E1~E6` 소계·`C*` 관급자재 제외구간·말단 내역 전수 편집기 + 보관함 1·2·3), 내역 도구 모달(`wITNum`, `wBoqRate`, `wM_Rate`, `wExpnd`, `wQtyList`) |
|
||||
| [`view_dan_ilwi.js`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/view_dan_ilwi.js) | 594 | `wM_Set_Sys`(`초기값 입력및수정` + `K_ORD` 판정기), `wM_JabPum`(`COSTN.DBF` 전수 단가목록표 + `wM_Boxa` 5개 가격지 비교 + `wM_Edt_Hal` 기준단가 비율보정), `wM_Edit_iLWi` / `wM_Edit_San`(`COSTN.SMT` 64B 블록 원문 전개 편집기 + `Q`/`Cm`/`&X00026L/M/S&` 실시간 계산기 + 카탈로그 브라우저) |
|
||||
| [`view_kanjub_prt.js`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/view_kanjub_prt.js) | 628 | `wM_KanJub`(`TWM_KANJUB` 16개 절사·단수·관급·이윤보정 콤보 + 4탭 `T/S/TA/SA`), `wM_KAN_RATE`(`TWM_KAN_RATE` 4탭 요율표 `RATE_R/RA/M/RATE` + `JB_R` 로더), `wStage`(`wM_Mk_Cont` 계약·`wM_Boq_ExecX` 실행·`wM_KanJub_K` 기성·설계변경), `PM_Prt`(`ST_Prt.CPM` 30종 보고서 + `SET_REP.DBF` 75종 양식 + CSV/조달청 XML/JDL 변환) |
|
||||
| [`view_prj_boq.js`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/view_prj_boq.js) | 189 | 계층 내역 재집계(`evaluateFullBoqRows` — `T1~T6` 공종헤더·`E1~E6` 소계·`C*` 관급자재 제외구간) · 단가 반영(`syncCostnUnitPrice`) · 저장 요청 묶음(`buildSavePayload`) — 화면은 `scr_*.js` |
|
||||
| [`app.js`](file:///D:/aislo-wt/안티그래비티/tools/stmate-test/app.js) | 688 | 상단 5대 메뉴·툴바·모달·9개 화면 라우팅 및 DBF/SMT/STC 양방향 저장 컨트롤러 |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""변환·가져오기 끼워 넣기 API — 조달청 XML 가져오기·변환 설정 · 엑셀 내역/역등록 · 기준 BOQ·자재·코드 엑셀 왕복 ·
|
||||
분리형 변경 STC · JB_* 기준 제비율표.
|
||||
분리형 변경 STC.
|
||||
|
||||
근거: 28번 §3(ST_U_Convert) · 30번(호환규정 XSD) · 24번 §3(ST_P_XLS CONV_XLS_2_ST · XLS_2_BDREC · MK_BSCBOQ_EXL) ·
|
||||
16번 §4.6(차수 인계) · 10번 §4(JB_* ↔ RATE*) · 10번(ST_U_ChgCode)
|
||||
16번 §4.6(차수 인계) · 10번(ST_U_ChgCode)
|
||||
쓰기는 tools/stmate-test/work_dbf/ 아래만(server.WORK_DBF_ROOT) · 엑셀·XML 원본은 읽기만.
|
||||
파일 받기: 경로(서버 PC) 또는 b64(화면에서 고른 파일).
|
||||
"""
|
||||
@@ -14,7 +14,6 @@ import base64
|
||||
import pathlib
|
||||
|
||||
from conv_jaje import build_jaje_xlsx, caps_from_ini
|
||||
from conv_jb import JB_DIR, list_jb, match_project, read_jb
|
||||
from conv_win8 import convert as convert_win8
|
||||
from conv_win8_ini import detect as detect_win8
|
||||
from conv_xls_apply import import_boq, register
|
||||
@@ -129,10 +128,6 @@ class SplitReq(BaseModel):
|
||||
target_folder_name: str = ""
|
||||
|
||||
|
||||
class JbReq(BaseModel):
|
||||
path: str
|
||||
|
||||
|
||||
class JajeSumReq(BaseModel):
|
||||
source_path: str = ""
|
||||
head: dict = {}
|
||||
@@ -280,23 +275,3 @@ def api_split_change(req: SplitReq):
|
||||
return _save(
|
||||
req.target_folder_name or f"{s.path.stem}_변경{rep['k_ord']}차", files, s, {"report": rep}
|
||||
)
|
||||
|
||||
|
||||
# ── JB_* 기준 제비율표(읽기 전용) ──
|
||||
@router.get("/api/conv/jb-list")
|
||||
def api_jb_list():
|
||||
return {"dir": str(JB_DIR), "files": list_jb()}
|
||||
|
||||
|
||||
@router.post("/api/conv/jb-read")
|
||||
def api_jb_read(req: JbReq):
|
||||
p = pathlib.Path(req.path).resolve()
|
||||
if JB_DIR.resolve() not in p.parents or not p.is_file():
|
||||
raise HTTPException(400, "설치 폴더 ST_BSC 의 JB_* 만 읽음")
|
||||
return read_jb(p)
|
||||
|
||||
|
||||
@router.post("/api/conv/jb-match")
|
||||
def api_jb_match(req: SrcReq):
|
||||
"""프로젝트 RATE* 표 ↔ 기준 JB 파일(셀 모두 같은 것 · 아니면 가장 가까운 것)."""
|
||||
return match_project(_src(req.source_path).files)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""보고서 내보내기 끼워 넣기 (api_report.py) — 시트 모형 → 엑셀 · CSV · 조달청 XML 검증.
|
||||
"""보고서 내보내기 끼워 넣기 (api_report.py) — 시트 모형 → 엑셀 · 조달청 XML 검증.
|
||||
|
||||
근거: 지식DB STmate/old/20_분석 19 §3 · 20 §3 · 30(조달청 XSD).
|
||||
시트 모형은 화면(scr_report.js)이 report_*.js 로 만들어 보냄 — 계산은 브라우저 엔진과 같은 코드.
|
||||
@@ -14,7 +14,7 @@ from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
from report_xlsx import build_csv, build_report_xlsx
|
||||
from report_xlsx import build_report_xlsx
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
XSD = (
|
||||
@@ -50,19 +50,6 @@ def api_report_xlsx(req: ReportReq):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/report/csv")
|
||||
def api_report_csv(req: ReportReq):
|
||||
if not req.sheets:
|
||||
raise HTTPException(status_code=400, detail="시트 모형 없음")
|
||||
body = "" + build_csv(req.sheets[0])
|
||||
name = req.filename if req.filename.endswith(".csv") else req.filename + ".csv"
|
||||
return Response(
|
||||
content=body.encode("utf-8"),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": _disposition(name)},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/report/validate-pps")
|
||||
def api_validate_pps(req: XmlReq):
|
||||
"""조달청 호환규정 XSD(V1.0.5) 검증 — 위반 줄을 돌려줌."""
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""설계 후속 단계 서버 — 공사(PRJLST 행)별 원가설정 · 단계 보고서 엑셀 내보내기 (api_stage.py).
|
||||
"""설계 후속 단계 서버 — 단계 보고서 엑셀 내보내기 (api_stage.py).
|
||||
|
||||
근거: 지식DB STmate/old/20_분석 27(계약·실행·기성·준공) · 16 §4.5(통합형 BOQ11 설계 + BOQ12 계약)
|
||||
근거: 지식DB STmate/old/20_분석 27(계약·실행·기성·준공)
|
||||
· 20 §3(SET_REP 보고서 0623·0624·0632~0638·0643~0648·0661·0673·0674)
|
||||
/api/stage/projects PRJLST 행마다 PCODE·CCODE·차수별 직접비·총공사비
|
||||
· 그 행 PRJ_MEMO 의 차수별 원가설정
|
||||
/api/stage/export-xlsx 화면 보고서 시트 모형(name·title·info·head·rows) → xlsx
|
||||
"""
|
||||
|
||||
@@ -16,19 +14,12 @@ from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from kanjub_cfg import build_kanjub_config, stage_slots
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Border, Font, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
from pydantic import BaseModel
|
||||
from stc_read import Source, prjlst_smt_all, table_rows
|
||||
|
||||
router = APIRouter()
|
||||
COST_COLS = re.compile(r"^(MCOST|LCOST|SCOST|TCOST|ACOST|COSTW)\d$")
|
||||
|
||||
|
||||
class StagePathReq(BaseModel):
|
||||
source_path: str
|
||||
|
||||
|
||||
class StageSheet(BaseModel):
|
||||
@@ -44,48 +35,6 @@ class StageXlsxReq(BaseModel):
|
||||
sheets: list[StageSheet]
|
||||
|
||||
|
||||
def _flat(sections: dict) -> 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()
|
||||
|
||||
+5
-278
@@ -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)
|
||||
: "<T3_C3B />";
|
||||
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)
|
||||
: `<div style="line-height:1.6;"><p><strong>1. 실행 모듈:</strong> <code>Start8x.exe</code> · <code>ST_Sub1.dll</code> · <code>ST_Prt.CPM</code></p><p><strong>2. DBF/SMT 직결:</strong> <code>COSTN.DBF</code> · <code>COSTN.SMT</code> · <code>BOQ11~14.DBF</code> · <code>PRJLST.DBF/.SMT</code> · <code>RATE_R/RA/M.DBF</code></p></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -37,7 +37,7 @@
|
||||
<footer id="stmate-statusbar" class="stmate-statusbar"></footer>
|
||||
</div>
|
||||
|
||||
<!-- 범용 모달 컨테이너 (wITNum, wBoqRate, wM_Rate, wExpnd, wM_Boxa, wM_Edt_Hal, 표준카탈로그 등) -->
|
||||
<!-- 범용 모달 컨테이너 (wITNum, wBoqRate, wM_Rate, wExpnd, wM_Boxa, wM_Edt_Hal 등) -->
|
||||
<div id="stmate-modal-backdrop" class="modal-backdrop hidden">
|
||||
<div id="stmate-modal-window" class="modal-window">
|
||||
<div class="modal-titlebar">
|
||||
@@ -57,8 +57,6 @@
|
||||
<script src="calc_a2w.js"></script>
|
||||
<script src="save_payload.js"></script>
|
||||
<script src="view_prj_boq.js"></script>
|
||||
<script src="view_dan_ilwi.js"></script>
|
||||
<script src="view_kanjub_prt.js"></script>
|
||||
<script src="app_modals.js"></script>
|
||||
<script src="ui_menu.js"></script>
|
||||
<script src="ui_app_menu.js"></script>
|
||||
|
||||
@@ -191,21 +191,3 @@ def build_report_xlsx(sheets: list[dict], formulas: bool = False, prn: dict | No
|
||||
buf = BytesIO()
|
||||
wb.save(buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def build_csv(sheet: dict) -> str:
|
||||
"""시트 하나 → CSV(쉼표 · 큰따옴표 감쌈 · 엑셀 한글용 BOM 은 호출 쪽)."""
|
||||
rows = sheet.get("rows") or []
|
||||
max_col = max(
|
||||
(column_index_from_string(k) for r in rows for k in _cells(r)),
|
||||
default=1,
|
||||
)
|
||||
out = []
|
||||
for row in rows:
|
||||
vals = []
|
||||
for c_idx in range(1, max_col + 1):
|
||||
v = row.get(get_column_letter(c_idx))
|
||||
s = "" if v is None else str(v)
|
||||
vals.append('"' + s.replace('"', '""') + '"' if re.search(r'[",\n]', s) else s)
|
||||
out.append(",".join(vals))
|
||||
return "\r\n".join(out) + "\r\n"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* STMATE w 8.0 — 변환·가져오기 끼워 넣기 (scr_conv.js) · 1번(파일) 메뉴
|
||||
* 조달청 XML 가져오기(Conv_XML_ST8) · XML 변환 설정(uM_XML_Info · uM_ConstType) · 엑셀 → 내역(wM_From_XLS) ·
|
||||
* 엑셀 역등록(CONV_XLS_2_ST) · 기준 BOQ·자재 기준자료·코드 변경표 엑셀 왕복 · 분리형 변경 STC · JB_* 제비율표
|
||||
* 엑셀 역등록(CONV_XLS_2_ST) · 기준 BOQ·자재 기준자료·코드 변경표 엑셀 왕복 · 분리형 변경 STC
|
||||
* 서버: api_conv.py — 쓰기는 work_dbf/ 아래만 · 파일은 화면에서 골라 base64 로 보냄
|
||||
*/
|
||||
(function (global) {
|
||||
@@ -182,23 +182,6 @@
|
||||
);
|
||||
await openSaved(r.saved_dir);
|
||||
},
|
||||
async jb(ctx) {
|
||||
const L = await req("/api/conv/jb-list");
|
||||
const m = ctx.currentSourcePath
|
||||
? await req("/api/conv/jb-match", { source_path: ctx.currentSourcePath })
|
||||
: {};
|
||||
const by = {};
|
||||
for (const f of L.files)
|
||||
by[`${f.kind} ${f.edition}`] = (by[`${f.kind} ${f.edition}`] || 0) + 1;
|
||||
alert(
|
||||
`[기준 제비율표 JB_*] ${L.files.length}개 (${L.dir})\n${Object.entries(by)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join("\n")}\n\n[이 공사 RATE 표 ↔ 기준]\n` +
|
||||
Object.entries(m)
|
||||
.map(([t, v]) => `${t}: ${v.jb} ${v.exact ? "(같음)" : `(다른 칸 ${v.diff_cells})`}`)
|
||||
.join("\n"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
async function run(act) {
|
||||
@@ -218,7 +201,6 @@
|
||||
["Act_ConvCodeXlsx", "코드 변경표 엑셀로 내보내기 (ST_U_ChgCode)", "codeXlsx"],
|
||||
["Act_ConvCodeChange", "코드 일괄 변경 (ST_U_ChgCode)", "codeChange"],
|
||||
["Act_ConvSplitChange", "분리형 변경 STC 만들기 — 차수 인계", "splitChange"],
|
||||
["Act_ConvJb", "기준 제비율표 JB_* 보기", "jb"],
|
||||
].forEach(([act, label, key]) => {
|
||||
PLUG.actions[act] = () => run(key);
|
||||
PLUG.menus.push({ menu: "m1", label, act });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* -*- coding: utf-8 -*- */
|
||||
/**
|
||||
* 보고서 출력 화면 (scr_report.js) — PM_Prt 대체 · 서식 목록(SET_REP 75종) · 미리보기 · 인쇄 · 엑셀/CSV · 조달청 XML
|
||||
* 보고서 출력 화면 (scr_report.js) — PM_Prt 대체 · 서식 목록(SET_REP 75종) · 미리보기 · 인쇄 · 엑셀 · 조달청 XML
|
||||
* 인쇄 선택(옵션·순서·영역) = scr_report_opts.js · 양식 변형(견본 없는 서식) = report_forms.js
|
||||
* 근거: 지식DB STmate/old/20_분석 20 §3(SET_REP) · 30(조달청 XSD) · old/30_원자료/print_form_map.txt
|
||||
* 계산 = calc.js 결과(ctx.computed) · 시트 모형 = report_*.js(엑셀 대조 시험 resources/tester/test_stmate_report_*.py)
|
||||
@@ -162,7 +162,6 @@
|
||||
<button class="win-btn" id="rp-print-all">인쇄(묶음 전체)</button>
|
||||
<button class="win-btn" id="rp-xlsx-all">엑셀 내보내기(전체)</button>
|
||||
<button class="win-btn" id="rp-xlsx-one">엑셀(이 시트)</button>
|
||||
<button class="win-btn" id="rp-csv">CSV(이 시트)</button>
|
||||
<button class="win-btn" id="rp-pps">조달청 XML</button>
|
||||
<span id="rp-msg"></span>
|
||||
</div>
|
||||
@@ -230,12 +229,6 @@
|
||||
() => msg("엑셀 저장"),
|
||||
(e) => msg(e.message),
|
||||
);
|
||||
if ($("rp-csv"))
|
||||
$("rp-csv").onclick = () =>
|
||||
post("/api/report/csv", [cur()], `${cur().name}.csv`, "text/csv").then(
|
||||
() => msg("CSV 저장"),
|
||||
(e) => msg(e.message),
|
||||
);
|
||||
if ($("rp-pps"))
|
||||
$("rp-pps").onclick = async () => {
|
||||
try {
|
||||
|
||||
@@ -95,14 +95,11 @@
|
||||
const conv = K.convert(tree, Object.assign({}, o, { pclass }), st, base);
|
||||
const designKj = S.kj(ctx, st);
|
||||
const designCfg = (ctx.state.kanjubStages || {})[st] || ctx.state.kanjubConfig;
|
||||
const cfg =
|
||||
o.useStored && o.storedCfg
|
||||
? o.storedCfg
|
||||
: K.defaultContractCfg(designCfg, designKj, {
|
||||
iyunRate: o.iyunRate === "" ? null : n0(o.iyunRate),
|
||||
taxFree: o.taxFree,
|
||||
rateM: K.effRates(Object.assign({}, o, { pclass })).M,
|
||||
});
|
||||
const cfg = K.defaultContractCfg(designCfg, designKj, {
|
||||
iyunRate: o.iyunRate === "" ? null : n0(o.iyunRate),
|
||||
taxFree: o.taxFree,
|
||||
rateM: K.effRates(Object.assign({}, o, { pclass })).M,
|
||||
});
|
||||
const kj = K.contractKanjub(conv, cfg);
|
||||
return { o, st, tree, conv, cfg, kj, designKj, pclass, preview: K.preview(conv) };
|
||||
}
|
||||
@@ -208,8 +205,7 @@
|
||||
}
|
||||
<span>단가 절사 자리</span>${inp("ct-dg", o.digits, 40)}
|
||||
<label><input type="checkbox" id="ct-gov" ${o.excludeGov ? "checked" : ""}/> 관급(C 구역) 제외</label>
|
||||
<button class="win-btn primary" id="ct-reg">계약자료 변환등록</button>
|
||||
<button class="win-btn" id="ct-stored">저장된 계약 공사 대조</button></div>
|
||||
<button class="win-btn primary" id="ct-reg">계약자료 변환등록</button></div>
|
||||
<div class="win-subbar">${OPTIONS.map(
|
||||
([k, l]) =>
|
||||
`<label style="margin-right:10px;"><input type="checkbox" class="ct-opt" data-k="${k}" ${o[k] ? "checked" : ""}/> ${l}</label>`,
|
||||
@@ -220,7 +216,6 @@
|
||||
<select id="ct-what" class="win-btn"><option value="direct" ${o.what === "direct" ? "selected" : ""}>직접공사비</option><option value="grand" ${o.what === "grand" ? "selected" : ""}>총공사비</option></select>
|
||||
${inp("ct-target", o.target, 130)}<button class="win-btn" id="ct-find">적용율 찾기</button>
|
||||
<span style="margin-left:12px;">계약 이윤율(%)</span>${inp("ct-iyun", o.iyunRate, 70)}
|
||||
${o.storedCfg ? `<label><input type="checkbox" id="ct-usestored" ${o.useStored ? "checked" : ""}/> 저장된 계약 원가설정 사용</label>` : ""}
|
||||
${o.found ? `<b style="margin-left:8px;">찾은 적용율 ${o.found.rate}% → ${S.fmt(o.found.value)}원</b>` : ""}</div>`;
|
||||
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: "" }) +
|
||||
`<p>「저장된 계약 원가설정 사용」을 켜면 원가계산서에 그 공사의 설정(이윤율·당초액 유지 표시)을 씀</p>`,
|
||||
);
|
||||
re();
|
||||
} catch (e) {
|
||||
alert("대조 실패: " + e.message);
|
||||
}
|
||||
});
|
||||
S.bindReports(ctx, api, builders(ctx));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ">")
|
||||
.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) => `
|
||||
<Worksheet ss:Name="${esc(name)}">
|
||||
<Table>
|
||||
<Row>${headers.map((h) => `<Cell><Data ss:Type="String">${esc(h)}</Data></Cell>`).join("")}</Row>
|
||||
${rows
|
||||
.map(
|
||||
(r) =>
|
||||
`<Row>${r
|
||||
.map((c) => {
|
||||
const isN = typeof c === "number" && Number.isFinite(c);
|
||||
return `<Cell><Data ss:Type="${isN ? "Number" : "String"}">${esc(c)}</Data></Cell>`;
|
||||
})
|
||||
.join("")}</Row>`,
|
||||
)
|
||||
.join("\n")}
|
||||
</Table>
|
||||
</Worksheet>`;
|
||||
|
||||
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 `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?mso-application progid="Excel.Sheet"?>
|
||||
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
||||
${shWon}
|
||||
${shBoq}
|
||||
${shIlwi}
|
||||
${shSan}
|
||||
${shJoong}
|
||||
${shDan}
|
||||
</Workbook>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. 조달청 공사원가 호환규정 V1.0.5 (Valid_Xml.vsl · T3_C3B) 전체 계층 XML 생성기
|
||||
*/
|
||||
function buildFullPpsXml(ctx) {
|
||||
const esc = (v) =>
|
||||
String(v ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.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 `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3_C3B xmlns="http://www.pps.go.kr/cost/v105" schemaVersion="1.0.5">
|
||||
<Header>
|
||||
<Program>STMATE w 8.0 (경영정보)</Program>
|
||||
<ProjectCode>${esc(ctx.state.projectMeta.pcode)}</ProjectCode>
|
||||
<ProjectName>${esc(ctx.state.projectMeta.pname)}</ProjectName>
|
||||
<PriceStandard>${esc(ctx.state.projectMeta.pnum)}</PriceStandard>
|
||||
<DirectMaterial>${bt.directM}</DirectMaterial>
|
||||
<DirectLabor>${bt.directL}</DirectLabor>
|
||||
<DirectExpense>${bt.directS}</DirectExpense>
|
||||
<DirectCostTotal>${bt.directT}</DirectCostTotal>
|
||||
<TotalSupplyAmount>${kj.FINAL_SUPPLY}</TotalSupplyAmount>
|
||||
<GovMaterialAmount>${kj.GOV_MAT}</GovMaterialAmount>
|
||||
<GrandTotalCost>${kj.GRAND_TOTAL}</GrandTotalCost>
|
||||
</Header>
|
||||
<CostSummary>
|
||||
${kj.lines
|
||||
.map(
|
||||
(ln) =>
|
||||
` <CostLine name="${esc(ln.label)}" base="${ln.base || 0}" rate="${ln.rate || 0}" amount="${ln.amount || 0}" />`,
|
||||
)
|
||||
.join("\n")}
|
||||
</CostSummary>
|
||||
<BoqItems count="${boqRows.length}">
|
||||
${boqRows
|
||||
.map(
|
||||
(r) =>
|
||||
` <BoqRow recno="${r._recno}" itnum="${esc(r.ITNUM)}" code="${esc(r.BDCODE1)}" name="${esc(r.NAME1)}" spec="${esc(r.SIZE1)}" unit="${esc(r.UNIT1)}" qty="${num(r.QTY1)}" mUnit="${num(r.MAMT1)}" lUnit="${num(r.LAMT1)}" sUnit="${num(r.SAMT1)}" tAmt="${num(r.T_AMT1)}" />`,
|
||||
)
|
||||
.join("\n")}
|
||||
</BoqItems>
|
||||
<UnitCostCatalog count="${costnRows.length}">
|
||||
${costnRows
|
||||
.map(
|
||||
(c) =>
|
||||
` <CostItem code="${esc(c.CODE)}" jdcode="${esc(c.JDCODE)}" name="${esc(c.NAME)}" spec="${esc(c.SIZE)}" unit="${esc(c.UNIT)}" m="${num(c.MAMT1)}" l="${num(c.LAMT1)}" s="${num(c.SAMT1)}" t="${num(c.TAMT1)}" />`,
|
||||
)
|
||||
.join("\n")}
|
||||
</UnitCostCatalog>
|
||||
</T3_C3B>`;
|
||||
}
|
||||
|
||||
global.STMATE_ENGINE = {
|
||||
evaluateSmtFormulaText,
|
||||
cascadeRecalculateAll,
|
||||
@@ -555,8 +300,5 @@
|
||||
qtyResolver,
|
||||
laborMap,
|
||||
findCycles,
|
||||
getRecipeBreakdown,
|
||||
buildMultiSheetExcelXml,
|
||||
buildFullPpsXml,
|
||||
};
|
||||
})(window);
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
@@ -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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>⚙️ 초기값 입력 및 수정 [wM_Set_Sys · PRJLST.DBF 초기값 변수 42종]</span>
|
||||
<span>노임환산식 좌→우 평가: ${evalRx.toFixed(6)}</span>
|
||||
</div>
|
||||
<div class="win-subbar">
|
||||
<button class="win-btn primary" id="sys-btn-apply">⚡ 초기값 적용 및 전 공정 재계산</button>
|
||||
<span style="color:#4b5563;">변경된 초기값은 기초단가·중기사용료·일위대가·내역서에 즉시 전파됩니다.</span>
|
||||
</div>
|
||||
|
||||
<div class="split-vert" style="overflow:auto;">
|
||||
<div class="pane-box" style="padding:10px;">
|
||||
<div class="pane-title"><span>1. 환율 및 소수 자리수 설정 (EXLNM1/2 · KCD · ACD · ZCD · JCD · ECD · BPN)</span></div>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="kanjub-field"><label>당초 달러환율(EXLNM1)</label><input type="number" id="sys-usd1" value="${sys.usdRate1}" /></div>
|
||||
<div class="kanjub-field"><label>변경 달러환율(EXLNM2)</label><input type="number" id="sys-usd2" value="${sys.usdRate2}" /></div>
|
||||
<div class="kanjub-field"><label>환율적용 단계</label><select id="sys-usd-mode"><option value="0" ${!sys.useChangedUsd ? "selected" : ""}>당초환율(EXLNM1)</option><option value="1" ${sys.useChangedUsd ? "selected" : ""}>변경환율(EXLNM2)</option></select></div>
|
||||
<div class="kanjub-field"><label>단가 자리수(ECD)</label><select id="sys-ecd"><option value="0" ${sys.ecdDigits === 0 ? "selected" : ""}>0자리(1원 정수)</option><option value="1" ${sys.ecdDigits === 1 ? "selected" : ""}>소수 1자리</option><option value="2" ${sys.ecdDigits === 2 ? "selected" : ""}>소수 2자리</option><option value="-1" ${sys.ecdDigits === -1 ? "selected" : ""}>10원 단위 절사</option></select></div>
|
||||
<div class="kanjub-field"><label>내역 차액보정(CHNNM)</label><select id="sys-chn"><option value="bimok" ${sys.chnMode === "bimok" ? "selected" : ""}>비목우선 (M+L+S=합계)</option><option value="total" ${sys.chnMode === "total" ? "selected" : ""}>총액우선 (단가×수량 차액 보정)</option></select></div>
|
||||
<div class="kanjub-field"><label>차액보정 순위(OPTNM)</label><select id="sys-opt"><option value="SML" ${sys.optOrder === "SML" ? "selected" : ""}>경비→재료→노무 (SML)</option><option value="MSL" ${sys.optOrder === "MSL" ? "selected" : ""}>재료→경비→노무 (MSL)</option><option value="LSM" ${sys.optOrder === "LSM" ? "selected" : ""}>노무→경비→재료 (LSM)</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane-box" style="padding:10px;">
|
||||
<div class="pane-title"><span>2. 노임 시간당 환산식 (RXNM_ · 좌→우 순차 평가 18번 §2.1) 및 특수노임 조건코드 (RX1~5CD)</span></div>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);">
|
||||
<div class="kanjub-field"><label>기본 노임환산식(RXNM_)</label><input type="text" id="sys-rx-formula" value="${sys.rxFormula}" /></div>
|
||||
<div class="kanjub-field"><label>환산 계수 적용 직종코드</label><input type="text" id="sys-cond-codes" value="${sys.condCodes}" /></div>
|
||||
</div>
|
||||
<p style="margin:6px 0 0;font-size:11px;color:#1e3a8a;">
|
||||
※ STMATE 고유 파서 검증: <code>1/8*16/12*25/20</code> → 좌→우 순차 평가 <code>0.20833333</code> (기계운전원 일급 267,360원 × 0.20833333 = <strong>55,700원/hr</strong> 정확 재현)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pane-box" style="padding:10px;">
|
||||
<div class="pane-title"><span>3. 현재 프로젝트 PRJLST.DBF 원본 초기값 필드</span></div>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(4,1fr);">
|
||||
<div class="kanjub-field"><label>공사명(PNAME)</label><input type="text" readonly value="${prj.PNAME || ""}" /></div>
|
||||
<div class="kanjub-field"><label>단가기준(PNUM)</label><input type="text" readonly value="${prj.PNUM || ""}" /></div>
|
||||
<div class="kanjub-field"><label>내역번호(PCODE)</label><input type="text" readonly value="BOQ${prj.PCODE || "11"}" /></div>
|
||||
<div class="kanjub-field"><label>진행단계(K_ORD)</label><input type="text" readonly value="${prj.K_ORD || "0"}" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>📋 단가목록표 [wM_JabPum · COSTN.DBF 전수 원장] — 총 ${rawCostn.length}건 중 ${filtered.length}건 표시</span>
|
||||
<span>5개 가격지 후보(AMT_11~51) 및 적용단가(TAMT1 = MAMT1 + LAMT1 + SAMT1)</span>
|
||||
</div>
|
||||
|
||||
<div class="win-subbar">
|
||||
${Object.keys(KIND_LABELS)
|
||||
.map(
|
||||
(k) => `
|
||||
<button class="win-btn dan-filter-btn ${filterKind === k ? "active" : ""}" data-filter="${k}">
|
||||
${KIND_LABELS[k]} (${counts[k] || 0})
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
<input type="text" id="dan-search-input" class="win-input" placeholder="명칭,규격 / 산근내용 검색..." value="${ctx.uiState.danSearch || ""}" style="width:185px;margin-left:auto;" />
|
||||
<button class="win-btn" id="dan-btn-boxa">🔍 가격지 비교(wM_Boxa)</button>
|
||||
<button class="win-btn" id="dan-btn-edthal">📐 기준단가 비율보정(wM_Edt_Hal)</button>
|
||||
<button class="win-btn" id="dan-btn-catalog">📚 표준카탈로그(894종)</button>
|
||||
</div>
|
||||
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:42px;">No</th>
|
||||
<th style="width:76px;">코드(CODE)</th>
|
||||
<th style="width:58px;">구분</th>
|
||||
<th>명칭 (NAME)</th>
|
||||
<th style="width:165px;">규격 (SIZE)</th>
|
||||
<th style="width:55px;">단위</th>
|
||||
<th style="width:98px;">재료단가(MAMT1)</th>
|
||||
<th style="width:98px;">노무단가(LAMT1)</th>
|
||||
<th style="width:98px;">경비단가(SAMT1)</th>
|
||||
<th style="width:105px;">합계단가(TAMT1)</th>
|
||||
<th style="width:82px;">1단가(조달)</th>
|
||||
<th style="width:82px;">2단가(물가)</th>
|
||||
<th style="width:82px;">3단가(거래)</th>
|
||||
<th style="width:82px;">산근/대가</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${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 `
|
||||
<tr data-code="${code}">
|
||||
<td class="center">${i + 1}</td>
|
||||
<td class="center"><strong>${code}</strong></td>
|
||||
<td class="center">${p}</td>
|
||||
<td>${r.NAME || ""}</td>
|
||||
<td>${r.SIZE || ""}</td>
|
||||
<td class="center">${r.UNIT || ""}</td>
|
||||
<td class="num">
|
||||
<input type="number" class="cell-input num dan-edit-m" data-code="${code}" value="${m1}" />
|
||||
</td>
|
||||
<td class="num">
|
||||
<input type="number" class="cell-input num dan-edit-l" data-code="${code}" value="${l1}" />
|
||||
</td>
|
||||
<td class="num">
|
||||
<input type="number" class="cell-input num dan-edit-s" data-code="${code}" value="${s1}" />
|
||||
</td>
|
||||
<td class="num"><strong>${fmt(t1)}</strong></td>
|
||||
<td class="num">${num(r.AMT_11) ? fmt(r.AMT_11) : "-"}</td>
|
||||
<td class="num">${num(r.AMT_21) ? fmt(r.AMT_21) : "-"}</td>
|
||||
<td class="num">${num(r.AMT_31) ? fmt(r.AMT_31) : "-"}</td>
|
||||
<td class="center">
|
||||
${
|
||||
hasMemo || p === "B" || p === "D" || p === "X"
|
||||
? `<button class="win-btn dan-jump-ilwi" data-code="${code}" style="height:19px;padding:0 5px;">산근보기</button>`
|
||||
: "-"
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>🧮 일위대가 · 단가산출근거 · 중기사용료 편집기 [wM_Edit_iLWi / wM_Edit_San / COSTN.SMT]</span>
|
||||
<span>선택 호표: ${selCode} ${selRow.NAME || ""} (SIxMemo 블록 #${memoObj ? memoObj.blockNo : "-"}, ${memoObj ? memoObj.byteLen : 0} bytes)</span>
|
||||
</div>
|
||||
|
||||
<div class="win-subbar">
|
||||
<button class="win-btn ilwi-sub-btn ${subFilter === "ALL_BDX" ? "active" : ""}" data-sub="ALL_BDX">전체 대가·산근·중기 (${bdxRows.length})</button>
|
||||
<button class="win-btn ilwi-sub-btn ${subFilter === "SMT_ONLY" ? "active" : ""}" data-sub="SMT_ONLY">COSTN.SMT 산식보유 (${Object.keys(memos).length})</button>
|
||||
<button class="win-btn ilwi-sub-btn ${subFilter === "D" ? "active" : ""}" data-sub="D">단가산출 D</button>
|
||||
<button class="win-btn ilwi-sub-btn ${subFilter === "B" ? "active" : ""}" data-sub="B">일위대가 B</button>
|
||||
<button class="win-btn ilwi-sub-btn ${subFilter === "X" ? "active" : ""}" data-sub="X">중기사용료 X</button>
|
||||
<button class="win-btn" id="ilwi-btn-load-recipe" style="margin-left:auto;">📥 표준 레시피(350종)/특수산식(21종) 불러오기</button>
|
||||
<button class="win-btn primary" id="ilwi-btn-apply-f9" data-code="${selCode}">⚡ F9 산식 계산결과를 호표단가에 반영</button>
|
||||
</div>
|
||||
|
||||
<div class="split-horiz">
|
||||
<!-- 좌측: B / D / X 호표 목록 -->
|
||||
<div class="pane-box" style="width:430px;">
|
||||
<div class="pane-title">
|
||||
<span>호표 마스터 목록 (클릭 시 우측 COSTN.SMT 전개식 로드)</span>
|
||||
<span>총 ${bdxRows.length}건</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:66px;">코드</th>
|
||||
<th>품명 (NAME)</th>
|
||||
<th style="width:95px;">규격</th>
|
||||
<th style="width:42px;">단위</th>
|
||||
<th style="width:82px;">합계단가</th>
|
||||
<th style="width:45px;">SMT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${bdxRows
|
||||
.map((r) => {
|
||||
const c = String(r.CODE || "").trim();
|
||||
const isSel = c === selCode;
|
||||
const hasM = Boolean(memos[c]);
|
||||
return `
|
||||
<tr class="ilwi-master-row ${isSel ? "selected" : ""}" data-code="${c}" style="cursor:pointer;">
|
||||
<td class="center"><strong>${c}</strong></td>
|
||||
<td>${r.NAME || ""}</td>
|
||||
<td>${r.SIZE || ""}</td>
|
||||
<td class="center">${r.UNIT || ""}</td>
|
||||
<td class="num"><strong>${fmt(num(r.TAMT1))}</strong></td>
|
||||
<td class="center">${hasM ? "📝" : "-"}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: COSTN.SMT 수식 편집기 + 실시간 평가 분석기 -->
|
||||
<div class="split-vert" style="flex:1;padding:0;">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>📝 COSTN.SMT 전개 텍스트 편집기 — <code>&X00026L&</code> 참조 · <code>Q=3600*q*K*f*E/Cm</code> · <code>:=A01</code> 문법 실시간 해석</span>
|
||||
<span>DB 저장단가: 재 ${fmt(num(selRow.MAMT1))} / 노 ${fmt(num(selRow.LAMT1))} / 경 ${fmt(num(selRow.SAMT1))} / 계 ${fmt(num(selRow.TAMT1))}원</span>
|
||||
</div>
|
||||
<textarea id="smt-formula-textarea" class="smt-editor-area" data-code="${selCode}" spellcheck="false">${currentSmtText}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>🔍 SIxMemo 수식 파서 실시간 평가 결과 (변수: ${
|
||||
Object.keys(evalRes.vars)
|
||||
.map((k) => `${k}=${evalRes.vars[k]}`)
|
||||
.join(", ") || "없음"
|
||||
})</span>
|
||||
<span>평가 합계: 재 ${fmt(evalRes.sumM)} + 노 ${fmt(evalRes.sumL)} + 경 ${fmt(evalRes.sumS)} = <strong>${fmt(evalRes.sumT)}원</strong></span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">행</th>
|
||||
<th>산식 원문 (COSTN.SMT Line)</th>
|
||||
<th style="width:150px;">변수 대입/평가</th>
|
||||
<th style="width:120px;">자원참조(&코드&)</th>
|
||||
<th style="width:95px;">참조단가</th>
|
||||
<th style="width:95px;">산출금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${evalRes.linesOut
|
||||
.map(
|
||||
(ln) => `
|
||||
<tr>
|
||||
<td class="center">${ln.lineNo}</td>
|
||||
<td style="font-family:Consolas,monospace;">${ln.raw}</td>
|
||||
<td style="color:#0050b3;font-weight:700;">${ln.assigned || ""}</td>
|
||||
<td class="center">${ln.refInfo ? `${ln.refInfo.refCode}[${ln.refInfo.compTag}] (${ln.refInfo.refName})` : ""}</td>
|
||||
<td class="num">${ln.refInfo ? fmt(ln.refInfo.baseUnitPrice) : ""}</td>
|
||||
<td class="num"><strong>${ln.refInfo ? fmt(ln.refInfo.calcAmt, 1) : ""}</strong></td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 단가/카탈로그 모달 생성기 (wM_Boxa / wM_Edt_Hal / 표준카탈로그 894코드·350레시피·21특수산식)
|
||||
*/
|
||||
function buildDanModalHtml(modalKind, ctx) {
|
||||
if (modalKind === "wM_Boxa") {
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wM_Boxa] 5개 가격지 단가 비교·일괄 채택</strong> — 자재 후보단가(AMT_11~51) 중 지정 슬롯 또는 최저가를 적용단가(AMT_61/MAMT1)로 일괄 채택합니다.</p>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);">
|
||||
<div class="kanjub-field"><label>채택 기준</label>
|
||||
<select id="boxa-adopt-mode">
|
||||
<option value="min">최소단가 (0원 제외 최저가 자동채택)</option>
|
||||
<option value="0">1단가 (조달청 가격정보 AMT_11)</option>
|
||||
<option value="1">2단가 (물가자료 AMT_21)</option>
|
||||
<option value="2">3단가 (물가정보 AMT_31)</option>
|
||||
<option value="3">4단가 (유통물가 AMT_41)</option>
|
||||
<option value="4">5단가 (거래가격 AMT_51)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-boxa">자재 적용단가 일괄 변경</button></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (modalKind === "wM_Edt_Hal") {
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;"><strong>[wM_Edt_Hal] 기준단가 비율 일괄 수정</strong> — <code>적용단가 = 기준단가 × 비율(%)</code> (적용단가 0원 및 보호항목 UC=T 제외).</p>
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="kanjub-field"><label>대상 비목</label><select id="edthal-kind"><option value="ALL">전체 (재·노·경)</option><option value="M">재료비(M)만</option><option value="L">노무비(L)만</option><option value="S">경비(S)만</option></select></div>
|
||||
<div class="kanjub-field"><label>적용 비율(%)</label><input type="number" id="edthal-pct" value="98" step="0.5" /></div>
|
||||
<div class="kanjub-field"><label>끝전 처리</label><select id="edthal-round"><option value="trunc">원단위 절사</option><option value="round">원단위 반올림</option></select></div>
|
||||
</div>
|
||||
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-edthal">기준단가 비율 일괄 수정 실행</button></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// 표준 카탈로그 (894 코드 · 350 일위대가 레시피 · 21 특수 단가산출)
|
||||
const cat = ctx.catalogData || { codes: [], recipes: [], special_san: [] };
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<p style="margin:0;">
|
||||
<strong>[STMATE 표준 카탈로그 브라우저]</strong> — <code>30_원자료</code>에서 추출한 표준코드 <strong>${cat.codes.length}종</strong> · 일위대가 레시피 <strong>${cat.recipes.length}종</strong> · 특수 단가산출 <strong>${cat.special_san.length}종</strong>
|
||||
</p>
|
||||
<div class="split-horiz" style="height:400px;padding:0;">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title"><span>특수 단가산출 21개 (Q식/사이클식 원문)</span></div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead><tr><th>호표</th><th>명칭</th><th>단위</th><th>Q식</th><th>편집기로 복사</th></tr></thead>
|
||||
<tbody>
|
||||
${(cat.special_san || [])
|
||||
.map(
|
||||
(s, idx) => `
|
||||
<tr>
|
||||
<td class="center">#${s["호표"]}</td>
|
||||
<td><strong>${s["명칭"]}</strong></td>
|
||||
<td class="center">${s["단위"]}</td>
|
||||
<td style="font-family:Consolas,monospace;">${(s["인자"] && s["인자"].Q) || ""}</td>
|
||||
<td class="center"><button class="win-btn btn-use-special-san" data-idx="${idx}">산식적용</button></td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title"><span>표준 일위대가 레시피 (${cat.recipes.length}종 중 상위 60종)</span></div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead><tr><th>구분</th><th>명칭</th><th>규격</th><th>단위</th><th>구성수</th></tr></thead>
|
||||
<tbody>
|
||||
${(cat.recipes || [])
|
||||
.slice(0, 60)
|
||||
.map(
|
||||
(r) => `
|
||||
<tr>
|
||||
<td class="center">${r["종류"]}</td>
|
||||
<td><strong>${r["명칭"]}</strong></td>
|
||||
<td>${r["규격"] || ""}</td>
|
||||
<td class="center">${r["단위"] || ""}</td>
|
||||
<td class="center">${(r["구성"] || []).length}행</td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
global.STMATE_VIEW_DAN_ILWI = {
|
||||
evaluateSmtFormulaText,
|
||||
renderWMSetSys,
|
||||
renderWMJabPum,
|
||||
renderWMEditIlwi,
|
||||
buildDanModalHtml,
|
||||
};
|
||||
})(window);
|
||||
@@ -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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>🏛️ 제잡비 계산 및 원가계산서 [TWM_KANJUB · PRJLST.SMT ${Object.keys(smt).length}개 키 연동]</span>
|
||||
<span>직접공사비: ${fmt(kj.DIRECT_COST)}원 → 도급액: ${fmt(kj.FINAL_SUPPLY)}원 + 관급자재: ${fmt(kj.GOV_MAT)}원 = 총공사비: ${fmt(kj.GRAND_TOTAL)}원</span>
|
||||
</div>
|
||||
|
||||
<!-- TWM_KANJUB 상단 툴바 -->
|
||||
<div class="win-subbar">
|
||||
<label>차수(cBx_Chasu):</label>
|
||||
<select id="kj-chasu" class="win-select">
|
||||
<option value="1">원설계 (1_R_*)</option>
|
||||
<option value="2">1차변경 (2_R_*)</option>
|
||||
<option value="3">2차변경 (3_R_*)</option>
|
||||
</select>
|
||||
<label>산정방식(cBx_W_Type):</label>
|
||||
<select id="kj-wtype" class="win-select">
|
||||
<option value="0" ${kc.scheme === "T" || kc.scheme === "S" ? "selected" : ""}>제잡비 원가계산방식</option>
|
||||
<option value="1" ${kc.scheme === "TA" || kc.scheme === "SA" ? "selected" : ""}>제잡비 표준시장방식(실적)</option>
|
||||
</select>
|
||||
<label>기준요율표(ST_BSC/*.JB_R):</label>
|
||||
<select id="kj-jbr-select" class="win-select" style="width:190px;">
|
||||
${jbrFiles.map((f) => `<option value="${f.path}" ${f.name.includes("2024") ? "selected" : ""}>${f.name}</option>`).join("")}
|
||||
</select>
|
||||
<button class="win-btn" id="kj-btn-apply-jbr">📥 JB_R 기준요율 적용</button>
|
||||
<button class="win-btn" id="kj-btn-open-rate">📊 제비율표 편집(wM_KAN_RATE)</button>
|
||||
<button class="win-btn primary" id="kj-btn-save-dbf" style="margin-left:auto;">💾 제잡비 계산결과 DBF/SMT 저장</button>
|
||||
</div>
|
||||
|
||||
<!-- Pn0_Input: 16개 제잡비 설정 콤보 패널 (22번 문서 §5.1) -->
|
||||
<div class="kanjub-16grid">
|
||||
<div class="kanjub-field"><label>1. 공사종류(PCLASS)</label><select id="kj-opt-pclass"><option value="0">일반토목/산림토목</option><option value="1">건축공사</option><option value="2">조경공사</option><option value="7">전문/설계변경</option></select></div>
|
||||
<div class="kanjub-field"><label>2. 공사기간(개월)</label><select id="kj-opt-term"><option value="2">6개월~12개월</option><option value="1">6개월 미만</option><option value="3">12개월 초과</option></select></div>
|
||||
<div class="kanjub-field"><label>3. 고용보험 등급</label><select id="kj-opt-kobo"><option value="1.01">1등급~특등급 (1.01%)</option><option value="0.9">5등급 이하 (0.90%)</option></select></div>
|
||||
<div class="kanjub-field"><label>4. 퇴직공제부금</label><select id="kj-opt-tbugum"><option value="0">적용안함 (0%)</option><option value="2.3">적용 (직접노무비×2.3%)</option></select></div>
|
||||
<div class="kanjub-field"><label>5. 산안비 관급포함</label><select id="kj-opt-anjun-gov"><option value="1" ${kc.anjunGovVatExcluded ? "selected" : ""}>관급자재(부가세제외 ÷1.1) 포함</option><option value="0" ${!kc.anjunGovVatExcluded ? "selected" : ""}>관급 미포함 (재+직노만)</option></select></div>
|
||||
<div class="kanjub-field"><label>6. 산안비 기초가산액</label><input type="number" id="kj-anjun-base" value="${kc.anjunBaseAdd}" step="1000" /></div>
|
||||
<div class="kanjub-field"><label>7. 산안비 1.2배 상한</label><select id="kj-anjun-cap"><option value="1" ${kc.anjunCap12 ? "selected" : ""}>MIN(가산식, 무가산×1.2) 적용</option><option value="0" ${!kc.anjunCap12 ? "selected" : ""}>상한 미적용</option></select></div>
|
||||
<div class="kanjub-field"><label>8. 간접비 절사단위</label><select id="kj-ind-trunc"><option value="0" ${kc.indirectTruncDigits === 0 ? "selected" : ""}>1원 단위 절사</option><option value="-1" ${kc.indirectTruncDigits === -1 ? "selected" : ""}>10원 단위 절사</option><option value="-3" ${kc.indirectTruncDigits === -3 ? "selected" : ""}>1,000원 단위 절사</option></select></div>
|
||||
<div class="kanjub-field"><label>9. 부가가치세 기준</label><select id="kj-vat-mode"><option value="supply_10" ${kc.vatMode === "supply_10" ? "selected" : ""}>(1) 총원가의 10%</option><option value="mat_10" ${kc.vatMode === "mat_10" ? "selected" : ""}>(2) 재료비의 10%(면세)</option></select></div>
|
||||
<div class="kanjub-field"><label>10. 도급액 절사(TOT_JEOL)</label><select id="kj-tot-jeol"><option value="-3" ${kc.totJeolDigits === -3 ? "selected" : ""}>1,000원 단위 절사</option><option value="-4" ${kc.totJeolDigits === -4 ? "selected" : ""}>10,000원 단위 절사</option><option value="0" ${kc.totJeolDigits === 0 ? "selected" : ""}>절사 안함(1원)</option></select></div>
|
||||
<div class="kanjub-field"><label>11. 이윤 세전 끝전조정</label><select id="kj-profit-adj"><option value="1" ${kc.enableProfitEndAdj ? "selected" : ""}>켬 (이윤에서 세전 역산 차감)</option><option value="0" ${!kc.enableProfitEndAdj ? "selected" : ""}>끔 (소계에서 단순 차감)</option></select></div>
|
||||
<div class="kanjub-field"><label>12. 관급자재 단수처리</label><select id="kj-gov-round"><option value="up" ${kc.govRoundMode === "up" ? "selected" : ""}>천원 올림(ROUNDUP,-3)</option><option value="down" ${kc.govRoundMode === "down" ? "selected" : ""}>천원 절사(ROUNDDOWN,-3)</option><option value="none" ${kc.govRoundMode === "none" ? "selected" : ""}>원단위 그대로</option></select></div>
|
||||
</div>
|
||||
|
||||
<!-- Pg_Ctrl: 하단 4탭 (일반형식 T / 수공형식 S / 실적일반 TA / 실적수공 SA) -->
|
||||
<div class="win-subbar" style="background:#dbeafe;">
|
||||
<strong>원가계산서 양식 탭 (Pg_Ctrl):</strong>
|
||||
${[
|
||||
{ id: "T", label: "탭1: 일반형식 (T · 재+노+경)" },
|
||||
{ id: "S", label: "탭2: 수공형식 (S · 경비 16세부비목)" },
|
||||
{ id: "TA", label: "탭3: 실적일반 (TA · 표준시장단가 병행)" },
|
||||
{ id: "SA", label: "탭4: 실적수공 (SA · 실적+수공)" },
|
||||
]
|
||||
.map(
|
||||
(t) => `
|
||||
<button class="win-btn kj-scheme-tab ${kc.scheme === t.id ? "active" : ""}" data-scheme="${t.id}">
|
||||
${t.label}
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
<span style="margin-left:auto;color:#1e3a8a;font-weight:700;">
|
||||
이윤 끝전조정: ${fmt(kj.PROFIT_ADJ)}원 (초기 이윤 ${fmt(kj.IYUN_INIT)}원 → 최종 이윤 ${fmt(kj.IYUN)}원)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45px;">순번</th>
|
||||
<th style="width:120px;">비목코드</th>
|
||||
<th style="width:220px;">원가계산서 항목명</th>
|
||||
<th>산정기초 공식 및 밑수 설명 (REA 03·04·16번 문서 검증)</th>
|
||||
<th style="width:140px;">산정기초 금액(원)</th>
|
||||
<th style="width:105px;">적용요율(%)</th>
|
||||
<th style="width:150px;">산출금액(원)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${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 `
|
||||
<tr class="${isStrong ? "boq-row-T" : ""}">
|
||||
<td class="center">${idx + 1}</td>
|
||||
<td class="center"><code>${ln.key}</code></td>
|
||||
<td><strong>${ln.label}</strong></td>
|
||||
<td>${ln.formula}</td>
|
||||
<td class="num">${ln.base !== null && ln.base !== undefined ? fmt(ln.base) : "-"}</td>
|
||||
<td class="num">
|
||||
${
|
||||
isRateEditable
|
||||
? `<input type="number" step="0.001" class="cell-input num kj-rate-input" data-key="${ln.key}" value="${ln.rate}" />`
|
||||
: ln.rate !== null && ln.rate !== undefined
|
||||
? fmt(ln.rate, 3) + "%"
|
||||
: "-"
|
||||
}
|
||||
</td>
|
||||
<td class="num"><strong>${fmt(ln.amount)}</strong></td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>📊 원가계산기준 제비율표 조회·수정 [TWM_KAN_RATE · ${activeTab} (${rows.length}행 × ${colKeys.length}열)]</span>
|
||||
<span>행20(선택적용) · 행45(산재/고용) · 행46(건보/연금/요양) 좌표 매핑 검증 완료</span>
|
||||
</div>
|
||||
|
||||
<div class="win-subbar">
|
||||
${[
|
||||
{ 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) => `
|
||||
<button class="win-btn kr-tab-btn ${activeTab === t.id ? "active" : ""}" data-tab="${t.id}">
|
||||
${t.label}
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
<select id="kr-jbr-select" class="win-select" style="margin-left:auto;width:210px;">
|
||||
${jbrFiles.map((f) => `<option value="${f.path}">${f.name}</option>`).join("")}
|
||||
</select>
|
||||
<button class="win-btn primary" id="kr-btn-load-jbr">📂 선택한 ST_BSC/*.JB_R 불러오기</button>
|
||||
</div>
|
||||
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45px;">행(0~)</th>
|
||||
<th style="width:240px;">STMATE 행 좌표 의미</th>
|
||||
${colKeys.map((ck) => `<th style="width:72px;">${ck}</th>`).join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows
|
||||
.map((r, rIdx) => {
|
||||
const highlight = ROW_MEANINGS[rIdx] ? "boq-row-T" : "";
|
||||
return `
|
||||
<tr class="${highlight}">
|
||||
<td class="center"><strong>#${rIdx}</strong></td>
|
||||
<td>${ROW_MEANINGS[rIdx] || `구간/공사종류 데이터 행 #${rIdx}`}</td>
|
||||
${colKeys.map((ck) => `<td class="num">${r[ck]}</td>`).join("")}
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>🔄 설계 후속 단계 관리 [27번 문서 — 계약내역(wM_Mk_Cont) · 실행예산(wM_Boq_ExecX) · 기성내역(wM_KanJub_K) · 설계변경 대비]</span>
|
||||
<span>K_ORD 단계 전이: 설계(0) → 계약(1) → 변경(2) → 실행(3) → 기성(4)</span>
|
||||
</div>
|
||||
|
||||
<div class="win-subbar">
|
||||
<button class="win-btn stage-tab-btn ${stageSub === "CONTRACT" ? "active" : ""}" data-st="CONTRACT">1. 계약내역 변환 (wM_Mk_Cont · 낙찰율/목표금액 역산)</button>
|
||||
<button class="win-btn stage-tab-btn ${stageSub === "EXEC" ? "active" : ""}" data-st="EXEC">2. 실행예산 (wM_Boq_ExecX · 실행단가 보정)</button>
|
||||
<button class="win-btn stage-tab-btn ${stageSub === "GISUNG" ? "active" : ""}" data-st="GISUNG">3. 기성내역/제잡비 (wM_KanJub_K · 전회/금회/누계)</button>
|
||||
<button class="win-btn stage-tab-btn ${stageSub === "REVISION" ? "active" : ""}" data-st="REVISION">4. 설계변경 증감대비표 (당초 vs 변경)</button>
|
||||
</div>
|
||||
|
||||
${
|
||||
stageSub === "CONTRACT"
|
||||
? `
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(4,1fr);">
|
||||
<div class="kanjub-field"><label>재료비 낙찰율(%)</label><input type="number" id="st-bid-m" value="${bidRateM}" step="0.001" /></div>
|
||||
<div class="kanjub-field"><label>노무비 낙찰율(%)</label><input type="number" id="st-bid-l" value="${bidRateL}" step="0.001" /></div>
|
||||
<div class="kanjub-field"><label>경비 낙찰율(%)</label><input type="number" id="st-bid-s" value="${bidRateS}" step="0.001" /></div>
|
||||
<div class="kanjub-field"><button class="win-btn primary" id="st-btn-apply-bid">계약단가(TAMT2) 일괄 산출</button></div>
|
||||
</div>
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>계약내역서 (설계단가 × 낙찰율 → 계약단가 및 계약금액 산출 · 적용제외 공정 보호 지원)</span>
|
||||
<span>설계 직공비: ${fmt(comp.boqComputed.totals.directT)}원 → 계약 직공비: ${fmt(Math.floor((comp.boqComputed.totals.directT * bidRateM) / 100))}원</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr><th>코드</th><th>공종명</th><th>규격</th><th>단위</th><th>수량</th><th>설계단가</th><th>설계금액</th><th>계약단가(낙찰적용)</th><th>계약금액</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${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 `
|
||||
<tr>
|
||||
<td class="center">${r.code}</td>
|
||||
<td>${r.name}</td>
|
||||
<td>${r.spec}</td>
|
||||
<td class="center">${r.unit}</td>
|
||||
<td class="num">${fmt(r.qty, 2)}</td>
|
||||
<td class="num">${fmt(r.tUnit)}</td>
|
||||
<td class="num">${fmt(r.tAmt)}</td>
|
||||
<td class="num"><strong>${fmt(cT)}</strong></td>
|
||||
<td class="num"><strong>${fmt(cAmt)}</strong></td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
stageSub === "EXEC"
|
||||
? `
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title"><span>[wM_Boq_ExecX] 실행예산 내역서 (계약금액 대비 현장 실행예산율 92.0% 기준 비교)</span></div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead><tr><th>코드</th><th>공종명</th><th>수량</th><th>설계금액</th><th>실행단가(92%)</th><th>실행예산금액</th><th>절감액(차액)</th></tr></thead>
|
||||
<tbody>
|
||||
${leafRows
|
||||
.map((r) => {
|
||||
const exUnit = Math.floor(r.tUnit * 0.92);
|
||||
const exAmt = Math.floor(r.qty * exUnit);
|
||||
return `
|
||||
<tr>
|
||||
<td class="center">${r.code}</td>
|
||||
<td>${r.name}</td>
|
||||
<td class="num">${fmt(r.qty, 2)}</td>
|
||||
<td class="num">${fmt(r.tAmt)}</td>
|
||||
<td class="num">${fmt(exUnit)}</td>
|
||||
<td class="num"><strong>${fmt(exAmt)}</strong></td>
|
||||
<td class="num" style="color:#137333;">${fmt(r.tAmt - exAmt)}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
stageSub === "GISUNG"
|
||||
? `
|
||||
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="kanjub-field"><label>전회 누적 기성율(%)</label><input type="number" id="st-prev-pct" value="${prevPct}" step="5" /></div>
|
||||
<div class="kanjub-field"><label>금회 포함 누계기성율(%)</label><input type="number" id="st-prog-pct" value="${progPct}" step="5" /></div>
|
||||
<div class="kanjub-field"><button class="win-btn primary" id="st-btn-apply-gisung">기성조서(전회·금회·누계·잔여) 재계산</button></div>
|
||||
</div>
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>[wM_KanJub_K] 기성내역 및 기성 제잡비 계산 (금회직공비 × 계약제잡비율 · 계약 | 전회 | 금회 | 누계 | 잔여)</span>
|
||||
<span>금회 기성율: ${(progPct - prevPct).toFixed(1)}% | 누계 기성도급액: ${fmt(Math.floor((comp.kanjubComputed.FINAL_SUPPLY * progPct) / 100))}원</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr><th>코드</th><th>공종명</th><th>계약수량</th><th>계약금액</th><th>전회기성액(${prevPct}%)</th><th>금회기성수량</th><th>금회기성액(${(progPct - prevPct).toFixed(1)}%)</th><th>누계기성액(${progPct}%)</th><th>잔여금액</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${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 `
|
||||
<tr>
|
||||
<td class="center">${r.code}</td>
|
||||
<td>${r.name}</td>
|
||||
<td class="num">${fmt(r.qty, 2)}</td>
|
||||
<td class="num">${fmt(r.tAmt)}</td>
|
||||
<td class="num">${fmt(prevA)}</td>
|
||||
<td class="num">${fmt(curQ, 2)}</td>
|
||||
<td class="num"><strong>${fmt(curA)}</strong></td>
|
||||
<td class="num"><strong>${fmt(cumA)}</strong></td>
|
||||
<td class="num">${fmt(r.tAmt - cumA)}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
stageSub === "REVISION"
|
||||
? `
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>설계변경 원가계산 대비표 (당초 도급액: ${fmt(comp.kanjubComputed.FINAL_SUPPLY)}원 vs 변경 도급액: ${fmt(revComp.revKj.FINAL_SUPPLY)}원 · 사후정산 건보/요양/연금 당초고정 지원)</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr><th>코드</th><th>공종명</th><th>당초수량(QTY1)</th><th>변경수량(QTY2)</th><th>단가</th><th>당초금액</th><th>변경금액</th><th>증감액(차액)</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${leafRows
|
||||
.map(
|
||||
(r) => `
|
||||
<tr>
|
||||
<td class="center">${r.code}</td>
|
||||
<td>${r.name}</td>
|
||||
<td class="num">${fmt(r.qty, 2)}</td>
|
||||
<td class="num"><input type="number" step="1" class="cell-input num rev-edit-qty2" data-id="${r.id}" value="${r.changedQty}" /></td>
|
||||
<td class="num">${fmt(r.tUnit)}</td>
|
||||
<td class="num">${fmt(r.tAmt)}</td>
|
||||
<td class="num"><strong>${fmt(r.changedTAmt)}</strong></td>
|
||||
<td class="num" style="color:${r.diffTAmt >= 0 ? "#0050b3" : "#c5221f"};"><strong>${fmt(r.diffTAmt)}</strong></td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>🖨️ 보고서 인쇄 및 엑셀/조달청 XML/JDL 변환 [PM_Prt · ST_Prt.CPM 30종 시트 · SET_REP.DBF ${setRepRows.length}종 양식]</span>
|
||||
<span>인쇄 옵션: 굴림체 · 비고=코드/호표 · 수량/단위 표준 · 일반양식</span>
|
||||
</div>
|
||||
|
||||
<div class="win-subbar">
|
||||
<label>보고서 양식(SET_REP):</label>
|
||||
<select id="prt-sheet-select" class="win-select" style="width:260px;">
|
||||
<option value="0608" ${activeRep === "0608" ? "selected" : ""}>[0608] 공사원가계산서 (PM_Won_A)</option>
|
||||
<option value="0601" ${activeRep === "0601" ? "selected" : ""}>[0601] 총괄설계내역서 (PM_Nae_Tot)</option>
|
||||
<option value="0603" ${activeRep === "0603" ? "selected" : ""}>[0603] 설계내역서 (PM_Nae)</option>
|
||||
<option value="0607" ${activeRep === "0607" ? "selected" : ""}>[0607] 관급자재내역서 (PM_Nae_Kwn)</option>
|
||||
<option value="0643" ${activeRep === "0643" ? "selected" : ""}>[0643] 설계변경내역서/증감대비표 (CHANGE)</option>
|
||||
<option value="0623" ${activeRep === "0623" ? "selected" : ""}>[0623/0673] 계약내역서·실행예산서</option>
|
||||
<option value="0633" ${activeRep === "0633" ? "selected" : ""}>[0633] 기성부분내역서·수량조서</option>
|
||||
<option value="0302" ${activeRep === "0302" ? "selected" : ""}>[0301/0302] 일위대가표·목록표 (PM_iLWi)</option>
|
||||
<option value="0402" ${activeRep === "0402" ? "selected" : ""}>[0401/0402] 단가산출근거·목록표 (Edit_SanKun)</option>
|
||||
<option value="0203" ${activeRep === "0203" ? "selected" : ""}>[0202/0203] 시간당중기사용료·목록표 (PM_JOONG)</option>
|
||||
<option value="0101" ${activeRep === "0101" ? "selected" : ""}>[0101~0105] 노무·재료·경비·단가대비표</option>
|
||||
<option value="0501" ${activeRep === "0501" ? "selected" : ""}>[0501~0507] 자원/중기/대가 수량금액집계표</option>
|
||||
<option value="0201" ${activeRep === "0201" ? "selected" : ""}>[0201] 환율및기초자료 · 〓 INITIAL 〓</option>
|
||||
<option value="SET_REP_ALL" ${activeRep === "SET_REP_ALL" ? "selected" : ""}>[전체] SET_REP.DBF 75종 양식 설정표</option>
|
||||
</select>
|
||||
<button class="win-btn primary" id="prt-btn-xls">📗 다중시트 엑셀(.xls 워크북) 내보내기</button>
|
||||
<button class="win-btn" id="prt-btn-csv">📊 현재 시트 CSV 다운로드</button>
|
||||
<button class="win-btn" id="prt-btn-xml">🏛️ 조달청 호환규정 XML(T3_C3B)</button>
|
||||
<button class="win-btn" id="prt-btn-jdl">📄 조달청 JDL 내보내기</button>
|
||||
<button class="win-btn" onclick="window.print()">🖨️ 인쇄</button>
|
||||
</div>
|
||||
|
||||
<div class="split-vert">
|
||||
<div class="pane-box" style="flex:1;padding:16px;overflow:auto;background:#fff;">
|
||||
<h2 style="text-align:center;margin:0 0 6px;letter-spacing:4px;color:#0f294d;">
|
||||
${
|
||||
{
|
||||
"0608": "공 사 원 가 계 산 서",
|
||||
"0601": "총 괄 설 계 내 역 서",
|
||||
"0603": "설 계 내 역 서",
|
||||
"0607": "관 급 자 재 내 역 서",
|
||||
"0643": "설 계 변 경 내 역 서 (증 감 대 비)",
|
||||
"0623": "계 약 내 역 서 · 실 행 예 산 서",
|
||||
"0633": "기 성 부 분 내 역 서 · 수 량 조 서",
|
||||
"0302": "일 위 대 가 표 (목 록)",
|
||||
"0402": "단 가 산 출 근 거",
|
||||
"0203": "시 간 당 중 기 사 용 료",
|
||||
"0101": "자 재 · 노 무 · 경 비 단 가 목 록 표",
|
||||
"0501": "자 원 · 중 기 · 대 가 수 량 금 액 집 계 표",
|
||||
"0201": "환 율 및 기 초 자 료 (〓 INITIAL 〓)",
|
||||
SET_REP_ALL: "SET_REP.DBF 75종 양식 설정표",
|
||||
}[activeRep] || "설 계 보 고 서"
|
||||
}
|
||||
</h2>
|
||||
<p style="text-align:center;margin:0 0 12px;font-weight:700;color:#334155;">
|
||||
공사명: ${ctx.state.projectMeta.pname} (총공사비: ${fmt(kj.GRAND_TOTAL)}원 / 도급액: ${fmt(kj.FINAL_SUPPLY)}원)
|
||||
</p>
|
||||
|
||||
${
|
||||
activeRep === "0608"
|
||||
? `<table class="dbgrid">
|
||||
<thead><tr><th>비목</th><th>산정기초(원)</th><th>요율(%)</th><th>금액(원)</th><th>산출근거</th></tr></thead>
|
||||
<tbody>
|
||||
${kj.lines
|
||||
.map(
|
||||
(ln) => `
|
||||
<tr>
|
||||
<td><strong>${ln.label}</strong></td>
|
||||
<td class="num">${ln.base ? fmt(ln.base) : ""}</td>
|
||||
<td class="num">${ln.rate !== null && ln.rate !== undefined ? fmt(ln.rate, 3) + "%" : ""}</td>
|
||||
<td class="num"><strong>${fmt(ln.amount)}</strong></td>
|
||||
<td>${ln.formula}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
["0601", "0603", "0607", "0643", "0623", "0633"].includes(activeRep)
|
||||
? `<table class="dbgrid">
|
||||
<thead><tr><th>공종</th><th>명칭</th><th>규격</th><th>단위</th><th>수량</th><th>재료비단가</th><th>노무비단가</th><th>경비단가</th><th>합계단가</th><th>합계금액</th><th>비고</th></tr></thead>
|
||||
<tbody>
|
||||
${boqRows
|
||||
.filter((r) =>
|
||||
activeRep === "0607"
|
||||
? r._isExcl
|
||||
: activeRep === "0601"
|
||||
? /^(T\d|E\d|C\d)$/.test(String(r.BDCODE1 || "").trim())
|
||||
: true,
|
||||
)
|
||||
.map(
|
||||
(r) => `
|
||||
<tr class="${r._rowClass}">
|
||||
<td class="center">${r.ITNUM || ""}</td>
|
||||
<td>${r.NAME1 || ""}</td>
|
||||
<td>${r.SIZE1 || ""}</td>
|
||||
<td class="center">${r.UNIT1 || ""}</td>
|
||||
<td class="num">${r.QTY1 ? fmt(r.QTY1, 2) : ""}</td>
|
||||
<td class="num">${r.MAMT1 ? fmt(r.MAMT1) : ""}</td>
|
||||
<td class="num">${r.LAMT1 ? fmt(r.LAMT1) : ""}</td>
|
||||
<td class="num">${r.SAMT1 ? fmt(r.SAMT1) : ""}</td>
|
||||
<td class="num">${r.TAMT1 ? fmt(r.TAMT1) : ""}</td>
|
||||
<td class="num"><strong>${r.T_AMT1 ? fmt(r.T_AMT1) : ""}</strong></td>
|
||||
<td class="center">${r.BDCODE1 || ""}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
activeRep === "0402"
|
||||
? Object.values(memos)
|
||||
.map(
|
||||
(m) => `
|
||||
<div style="border:1px solid #94a3b8;padding:8px;margin-bottom:8px;">
|
||||
<div style="font-weight:700;background:#eef2f8;padding:4px 8px;margin-bottom:4px;">[${m.code}] ${(costnRows.find((x) => x.CODE === m.code) || {}).NAME || ""}</div>
|
||||
<pre style="margin:0;font-family:Consolas,monospace;font-size:11.5px;white-space:pre-wrap;">${m.text}</pre>
|
||||
</div>`,
|
||||
)
|
||||
.join("")
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
["0302", "0203", "0101", "0501", "0201"].includes(activeRep)
|
||||
? `<table class="dbgrid">
|
||||
<thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>재료비</th><th>노무비</th><th>경비</th><th>합계단가</th></tr></thead>
|
||||
<tbody>
|
||||
${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) => `
|
||||
<tr>
|
||||
<td class="center"><strong>${r.CODE}</strong></td>
|
||||
<td>${r.NAME || ""}</td>
|
||||
<td>${r.SIZE || ""}</td>
|
||||
<td class="center">${r.UNIT || ""}</td>
|
||||
<td class="num">${fmt(num(r.MAMT1))}</td>
|
||||
<td class="num">${fmt(num(r.LAMT1))}</td>
|
||||
<td class="num">${fmt(num(r.SAMT1))}</td>
|
||||
<td class="num"><strong>${fmt(num(r.TAMT1))}</strong></td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
activeRep === "SET_REP_ALL"
|
||||
? `<table class="dbgrid">
|
||||
<thead><tr><th>양식코드(INDX)</th><th>보고서명(NAME)</th><th>인쇄 표제(HEADTITLE)</th><th>컬럼수(FLDCNT)</th><th>필드1</th><th>필드2</th><th>필드3</th><th>필드4</th></tr></thead>
|
||||
<tbody>
|
||||
${setRepRows
|
||||
.map(
|
||||
(r) => `
|
||||
<tr>
|
||||
<td class="center"><strong>${r.INDX}</strong></td>
|
||||
<td>${r.NAME}</td>
|
||||
<td><strong>${r.HEADTITLE}</strong></td>
|
||||
<td class="center">${r.FLDCNT}</td>
|
||||
<td>${r.FLD_N1 || ""}</td>
|
||||
<td>${r.FLD_N2 || ""}</td>
|
||||
<td>${r.FLD_N3 || ""}</td>
|
||||
<td>${r.FLD_N4 || ""}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
global.STMATE_VIEW_KANJUB_PRT = {
|
||||
renderWMKanJub,
|
||||
renderWMKanRate,
|
||||
renderWStage,
|
||||
renderPMPrt,
|
||||
};
|
||||
})(window);
|
||||
@@ -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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>📁 공사목록표 [TWPRJLIST · PRJLST.DBF / PRJLST.SMT] — 총 ${sources.length}개 프로젝트 연결됨</span>
|
||||
<span>더블클릭 시 설계내역서(wBoq) 진입</span>
|
||||
</div>
|
||||
|
||||
<!-- TWPRJLIST 상단 액션 툴바 -->
|
||||
<div class="win-subbar">
|
||||
<button class="win-btn primary" id="prj-btn-open-boq">📐 해당분 수정 (설계내역서 wBoq)</button>
|
||||
<button class="win-btn" id="prj-btn-kanjub">🏛️ 제잡비 계산 (wM_KanJub)</button>
|
||||
<button class="win-btn" id="prj-btn-edit-meta">✏️ 등록정보 수정</button>
|
||||
<button class="win-btn" data-file-act="copy">📋 복사등록 (차수/단계 변환)</button>
|
||||
<button class="win-btn" data-file-act="pack">🗜️ 해당분만 압축하기 (.STC 생성)</button>
|
||||
<button class="win-btn" data-file-act="new">🆕 새 공사</button>
|
||||
<button class="win-btn" data-file-act="check">🔍 STC 검사</button>
|
||||
<button class="win-btn" data-file-act="backups">🗂️ 백업·복원</button>
|
||||
<button class="win-btn ${showPreview ? "active" : ""}" id="prj-btn-toggle-preview">
|
||||
👁️ 내역서 미리보기 ${showPreview ? "숨기기" : "표시"}
|
||||
</button>
|
||||
<span style="margin-left:auto;color:#1e3a8a;font-weight:700;">
|
||||
현재 선택: ${prj.PNAME || ctx.state.projectMeta.pname} (직공비 ${fmt(directCost)}원 / 총공사비 ${fmt(totalCost)}원)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="split-vert">
|
||||
<!-- 상단 RGrdPrjList: 공사목록표 그리드 -->
|
||||
<div class="pane-box" style="flex: ${showPreview ? "0 0 44%" : "1"};">
|
||||
<div class="pane-title">
|
||||
<span>RGrdPrjList — PC 내 STMATE 프로젝트 원장 (클릭: 프로젝트 로드 / 더블클릭: 내역서 편집)</span>
|
||||
<span>K_ORD 단계: ${KORD_BADGES[num(prj.K_ORD)] || "설계내역(0)"}</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:42px;">순번</th>
|
||||
<th style="width:95px;">구분</th>
|
||||
<th style="width:105px;">공사번호(PNUM)</th>
|
||||
<th>공사명 (PNAME / 아카이브명)</th>
|
||||
<th style="width:65px;">코드(PCODE)</th>
|
||||
<th style="width:120px;">공사종류(PCLASS)</th>
|
||||
<th style="width:95px;">단계(K_ORD)</th>
|
||||
<th style="width:125px;">직공비(TCOST1)</th>
|
||||
<th style="width:125px;">총공사비(COSTW1)</th>
|
||||
<th style="width:110px;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${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 `
|
||||
<tr class="prj-row ${isCur ? "selected" : ""}" data-path="${src.path}">
|
||||
<td class="center">${idx + 1}</td>
|
||||
<td class="center"><strong>${src.kind.toUpperCase()}</strong></td>
|
||||
<td class="center">${pnum}</td>
|
||||
<td><strong>${src.name}</strong></td>
|
||||
<td class="center">BOQ${pcode}</td>
|
||||
<td class="center">${pcls}</td>
|
||||
<td class="center">${kord}</td>
|
||||
<td class="num"><strong>${tcost}</strong></td>
|
||||
<td class="num"><strong>${costw}</strong></td>
|
||||
<td class="center">
|
||||
<button class="win-btn btn-load-prj" data-path="${src.path}">
|
||||
${isCur ? "열림 (내역이동)" : "불러오기"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${renderPrjRows(ctx)}
|
||||
|
||||
${
|
||||
showPreview
|
||||
? `
|
||||
<!-- 하단 RDbGrid_Boq: 내역서 미리보기 그리드 -->
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>RDbGrid_Boq — 내역서 미리보기 (${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"} · 총 ${boqRows.length}행 전수 표시)</span>
|
||||
<span>공종(ITNUM) | 명칭(NAME1) | 규격(SIZE1) | 수량(QTY1) | 단위(UNIT1) | 단가(TAMT1) | 금액(T_AMT1) | 비고(BDCODE1)</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45px;">행번</th>
|
||||
<th style="width:70px;">공종(ITNUM)</th>
|
||||
<th>명칭 (NAME1)</th>
|
||||
<th style="width:170px;">규격 (SIZE1)</th>
|
||||
<th style="width:90px;">수량 (QTY1)</th>
|
||||
<th style="width:65px;">단위</th>
|
||||
<th style="width:110px;">합계단가 (TAMT1)</th>
|
||||
<th style="width:125px;">합계금액 (T_AMT1)</th>
|
||||
<th style="width:90px;">비고 (BDCODE1)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${boqRows
|
||||
.map(
|
||||
(r, i) => `
|
||||
<tr class="${r._rowClass}">
|
||||
<td class="center">${i + 1}</td>
|
||||
<td class="center">${r.ITNUM || ""}</td>
|
||||
<td>${r.NAME1 || ""}</td>
|
||||
<td>${r.SIZE1 || ""}</td>
|
||||
<td class="num">${r.QTY1 ? fmt(r.QTY1, 2) : ""}</td>
|
||||
<td class="center">${r.UNIT1 || ""}</td>
|
||||
<td class="num">${r.TAMT1 ? fmt(r.TAMT1) : ""}</td>
|
||||
<td class="num"><strong>${r.T_AMT1 ? fmt(r.T_AMT1) : ""}</strong></td>
|
||||
<td class="center">${r.BDCODE1 || ""}</td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 열린 프로젝트의 공사(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 `<tr><td class="center"><input type="radio" name="prj-pcode" value="${pc}" ${pc === pick ? "checked" : ""} /></td><td class="center">BOQ${pc}</td><td>${r.PNAME || ""}</td><td class="center">${KORD_BADGES[num(r.K_ORD)] || r.K_ORD || ""}</td><td class="num">${fmt(num(r.TCOST1))}</td><td class="num">${fmt(num(r.TCOST2))}</td><td class="num">${fmt(num(r.COSTW1))}</td></tr>`;
|
||||
};
|
||||
return `<div class="pane-box" style="flex:0 0 auto;max-height:120px;"><div class="pane-title"><span>공사 (PRJLST ${rows.length}행) — 고른 공사로 복사등록·해당분 압축</span></div><div class="grid-scroll"><table class="dbgrid"><thead><tr><th style="width:36px;">고름</th><th style="width:70px;">표</th><th>공사명</th><th style="width:95px;">단계(K_ORD)</th><th style="width:120px;">직공비 1차</th><th style="width:120px;">직공비 2차</th><th style="width:120px;">총공사비 1차</th></tr></thead><tbody>${rows.map(tr).join("")}</tbody></table></div></div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<div class="win-form">
|
||||
<div class="win-form-header">
|
||||
<span>📐 설계내역서 편집 [wBoq · ${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"}] — 전체 ${allRows.length}행 (표시 ${filtered.length}행)</span>
|
||||
<span>직공비 합계: ${fmt(totals.directT)}원 | 관급·제외구역(C*): ${fmt(totals.govMatTotal + totals.govFeeTotal)}원</span>
|
||||
</div>
|
||||
|
||||
<!-- wBoq 상단 도구바 -->
|
||||
<div class="win-subbar">
|
||||
<label>수준필터(cBox01):</label>
|
||||
<select id="boq-level-filter" class="win-select">
|
||||
<option value="ALL" ${lvFilter === "ALL" ? "selected" : ""}>내역서 전체 (${allRows.length}행)</option>
|
||||
<option value="STRUCT" ${lvFilter === "STRUCT" ? "selected" : ""}>공종제목·집계행만 (T1~T6 / E1~E6 / C*)</option>
|
||||
<option value="LEAF" ${lvFilter === "LEAF" ? "selected" : ""}>순수 산출공종 말단행만</option>
|
||||
<option value="EXCL" ${lvFilter === "EXCL" ? "selected" : ""}>관급·제외금액 구역(C*)만</option>
|
||||
</select>
|
||||
<input type="text" id="boq-search-input" class="win-input" placeholder="공종명/규격/코드 검색..." value="${ctx.uiState.boqSearch || ""}" style="width:170px;" />
|
||||
<button class="win-btn" id="boq-btn-itnum">🔢 공종번호 자동입력(wITNum)</button>
|
||||
<button class="win-btn" id="boq-btn-rate">📈 할증율 적용(wBoqRate)</button>
|
||||
<button class="win-btn" id="boq-btn-mrate">✂️ 비목단수보정(wM_Rate)</button>
|
||||
<button class="win-btn" id="boq-btn-expnd">📂 일위대가 풀기(wExpnd)</button>
|
||||
<button class="win-btn" id="boq-btn-add-leaf">+ 신규 내역행 추가</button>
|
||||
<button class="win-btn primary" id="boq-btn-save-dbf">💾 DBF/STC 즉시 반영</button>
|
||||
</div>
|
||||
|
||||
<div class="split-horiz">
|
||||
<!-- 좌측 메인 BOQ 계층 그리드 -->
|
||||
<div class="pane-box" style="flex:1;">
|
||||
<div class="pane-title">
|
||||
<span>계층 설계내역서 원장 (수량 수정 시 비목별 ROUNDDOWN 및 T*/E*/C* 집계 즉시 재계산 · 호표코드 더블클릭 시 대가표 이동)</span>
|
||||
<span>차액보정: ${ctx.state.sysConfig.chnMode === "total" ? "총액우선(OPTNM=" + ctx.state.sysConfig.optOrder + ")" : "비목우선"}</span>
|
||||
</div>
|
||||
<div class="grid-scroll">
|
||||
<table class="dbgrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">No</th>
|
||||
<th style="width:65px;">공종(ITNUM)</th>
|
||||
<th style="width:82px;">코드(BDCODE1)</th>
|
||||
<th>명칭 (NAME1)</th>
|
||||
<th style="width:145px;">규격 (SIZE1)</th>
|
||||
<th style="width:55px;">단위</th>
|
||||
<th style="width:85px;">수량(QTY1)</th>
|
||||
<th style="width:92px;">재료비단가</th>
|
||||
<th style="width:92px;">노무비단가</th>
|
||||
<th style="width:92px;">경비단가</th>
|
||||
<th style="width:98px;">합계단가</th>
|
||||
<th style="width:105px;">재료비금액</th>
|
||||
<th style="width:105px;">노무비금액</th>
|
||||
<th style="width:105px;">경비금액</th>
|
||||
<th style="width:115px;">합계금액(T_AMT1)</th>
|
||||
<th style="width:58px;">보관함</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${filtered
|
||||
.map((r) => {
|
||||
const bd = String(r.BDCODE1 || "").trim();
|
||||
const isLeaf = bd && !isStructCode(bd);
|
||||
return `
|
||||
<tr class="${r._rowClass}" data-recno="${r._recno}" data-bdcode="${bd}">
|
||||
<td class="center">${r._idx + 1}</td>
|
||||
<td class="center">
|
||||
<input type="text" class="cell-input center boq-edit-itnum" data-recno="${r._recno}" value="${r.ITNUM || ""}" />
|
||||
</td>
|
||||
<td class="center">
|
||||
${
|
||||
isLeaf
|
||||
? `<a href="#" class="boq-jump-code" data-code="${bd}" style="color:#0050b3;font-weight:700;text-decoration:underline;">${bd}</a>`
|
||||
: `<strong>${bd}</strong>`
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="cell-input boq-edit-name" data-recno="${r._recno}" value="${r.NAME1 || ""}" />
|
||||
</td>
|
||||
<td>${r.SIZE1 || ""}</td>
|
||||
<td class="center">${r.UNIT1 || ""}</td>
|
||||
<td class="num">
|
||||
${
|
||||
isLeaf
|
||||
? `<input type="number" step="0.01" class="cell-input num boq-edit-qty" data-recno="${r._recno}" data-code="${bd}" value="${r.QTY1}" />`
|
||||
: ""
|
||||
}
|
||||
</td>
|
||||
<td class="num">${r.MAMT1 ? fmt(r.MAMT1) : ""}</td>
|
||||
<td class="num">${r.LAMT1 ? fmt(r.LAMT1) : ""}</td>
|
||||
<td class="num">${r.SAMT1 ? fmt(r.SAMT1) : ""}</td>
|
||||
<td class="num"><strong>${r.TAMT1 ? fmt(r.TAMT1) : ""}</strong></td>
|
||||
<td class="num">${r.M_AMT1 ? fmt(r.M_AMT1) : ""}</td>
|
||||
<td class="num">${r.L_AMT1 ? fmt(r.L_AMT1) : ""}</td>
|
||||
<td class="num">${r.S_AMT1 ? fmt(r.S_AMT1) : ""}</td>
|
||||
<td class="num"><strong>${r.T_AMT1 ? fmt(r.T_AMT1) : ""}</strong></td>
|
||||
<td class="center">
|
||||
${
|
||||
isLeaf
|
||||
? `<button class="win-btn boq-put-pocket" data-code="${bd}" data-name="${r.NAME1 || ""}" data-qty="${r.QTY1}" style="height:19px;padding:0 5px;">담기</button>`
|
||||
: ""
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측 내역보관함 (1)(2)(3) 패널 (22번 문서 §4) -->
|
||||
<div class="pane-box" style="width:270px;">
|
||||
<div class="pane-title">
|
||||
<span>📦 내역보관함 (${pocketIdx})</span>
|
||||
<div>
|
||||
${[1, 2, 3]
|
||||
.map(
|
||||
(n) =>
|
||||
`<button class="win-btn pocket-tab-btn ${pocketIdx === n ? "active" : ""}" data-pocket="${n}" style="height:20px;padding:0 6px;">(${n})</button>`,
|
||||
)
|
||||
.join(" ")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-scroll" style="padding:6px;">
|
||||
<p style="margin:0 0 6px;color:#4b5563;font-size:11px;">
|
||||
자주 쓰는 공종을 보관함 (1)~(3)에 담아두고 현재 내역서에 즉시 삽입합니다.
|
||||
</p>
|
||||
${
|
||||
(pockets[pocketIdx] || []).length === 0
|
||||
? `<div style="padding:18px 8px;text-align:center;color:#6b7280;">보관함 (${pocketIdx})이 비어 있습니다.<br/>내역 행 우측 [담기]를 클릭하세요.</div>`
|
||||
: `<table class="dbgrid">
|
||||
<thead><tr><th>코드</th><th>품명</th><th>수량</th><th>삽입</th></tr></thead>
|
||||
<tbody>
|
||||
${(pockets[pocketIdx] || [])
|
||||
.map(
|
||||
(p, pi) => `
|
||||
<tr>
|
||||
<td class="center">${p.code}</td>
|
||||
<td>${p.name}</td>
|
||||
<td class="num">${fmt(p.qty, 2)}</td>
|
||||
<td class="center"><button class="win-btn pocket-insert-btn" data-idx="${pi}" style="height:19px;padding:0 5px;">넣기</button></td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 내역 도구 모달 HTML 생성기 (wITNum / wBoqRate / wM_Rate / wExpnd / wQtyList)
|
||||
*/
|
||||
function buildBoqToolModalHtml(toolKind, ctx) {
|
||||
if (toolKind === "wITNum") {
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wITNum] 내역서 공종번호 자동입력</strong> — 대공종(T4)/중공종(T2)/말단 공종행에 번호체계를 일괄 부여합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>대공종(T4) 번호형식</label><select id="itnum-t4-fmt"><option value="1.">1. / 2. / 3.</option><option value="I.">I. / II. / III.</option><option value="가.">가. / 나. / 다.</option></select></div><div class="kanjub-field"><label>중공종(T2) 번호형식</label><select id="itnum-t2-fmt"><option value="1.1">1.1 / 1.2 / 1.3</option><option value="(1)">(1) / (2) / (3)</option></select></div><div class="kanjub-field"><label>말단 공종행 번호부여</label><select id="itnum-leaf-fmt"><option value="none">공란 유지 (STMATE 기본)</option><option value="01">01, 02, 03...</option><option value="-1">-1, -2, -3...</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-itnum">공종번호 일괄 부여 실행</button></div></div>`;
|
||||
}
|
||||
if (toolKind === "wBoqRate") {
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wBoqRate] 내역 항목 할증율 일괄 적용</strong> — 전체 또는 비목별(재·노·경) 할증 비율(%)을 적용합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>재료비 할증(%)</label><input type="number" id="boqrate-m" value="100" step="1" /></div><div class="kanjub-field"><label>노무비 할증(%)</label><input type="number" id="boqrate-l" value="100" step="1" /></div><div class="kanjub-field"><label>경비 할증(%)</label><input type="number" id="boqrate-s" value="100" step="1" /></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-boqrate">내역 할증율 적용</button></div></div>`;
|
||||
}
|
||||
if (toolKind === "wM_Rate") {
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wM_Rate] 선택 범위 수량·비목 단수 일괄 보정</strong> — 절사/반올림 및 소수점 자리수(0~4위)를 일괄 보정합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);"><div class="kanjub-field"><label>보정 방식</label><select id="mrate-mode"><option value="trunc">절사 (ROUNDDOWN)</option><option value="round">반올림 (ROUND)</option></select></div><div class="kanjub-field"><label>소수 자리수(ECD)</label><select id="mrate-digits"><option value="0">소수 0위 (정수 1원)</option><option value="1">소수 1위 (0.1)</option><option value="2">소수 2위 (0.01)</option><option value="-1">10원 단위 절사</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-mrate">단수 일괄 보정 적용</button></div></div>`;
|
||||
}
|
||||
if (toolKind === "wExpnd") {
|
||||
const bdxList = (ctx.state.ilwiItems || []).slice(0, 30);
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wExpnd / wM_Expnd] 일위대가·단가산출 내역서 풀어넣기</strong> — 선택한 호표의 재료·노무·경비 구성행을 내역서에 직접 전개합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>전개할 호표 선택</label><select id="expnd-code">${bdxList.map((x) => `<option value="${x.code}">${x.code} ${x.rawName || x.name}</option>`).join("")}</select></div><div class="kanjub-field"><label>집계 구분선(RGrpToT)</label><select id="expnd-tot"><option value="E1">소계(=)</option><option value="E2">계(==)</option><option value="E3">합계(===)</option></select></div><div class="kanjub-field"><label>단가 끝전 처리</label><select id="expnd-round"><option value="trunc">절삭하여 소수 0위 적용</option><option value="round">반올림하여 소수 0위 적용</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-expnd">일위대가 풀어넣기 실행</button></div></div>`;
|
||||
}
|
||||
const leafRows = ctx.computed.boqComputed.rows || [];
|
||||
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wQtyList] 수량산출서(QCODE.DBF) 코드별 수량 연결·집계</strong> — 수량산출식 결과를 내역서 QTY1에 즉시 반영합니다.</p><div class="grid-scroll" style="max-height:320px;"><table class="dbgrid"><thead><tr><th>코드</th><th>공종명</th><th>현재 내역수량</th><th>산출근거 수식 (예: 3.46*1000)</th><th>반영</th></tr></thead><tbody>${leafRows
|
||||
.slice(0, 25)
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr><td class="center"><strong>${r.code}</strong></td><td>${r.name}</td><td class="num">${fmt(r.qty, 2)}</td><td><input type="text" class="win-input qty-expr-input" data-id="${r.id}" value="${r.qty}" style="width:100%;" /></td><td class="center"><button class="win-btn btn-apply-qty-expr" data-id="${r.id}">산식반영</button></td></tr>`,
|
||||
)
|
||||
.join("")}</tbody></table></div></div>`;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user