wip(stmate-test): 개발 중 저장 — 전역 깃 싱크(요구 686-2) · tools/stmate-test
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# STMATE w 8.0 테스트 앱 계획서 및 검증 아카이브
|
||||
|
||||
이 문서는 `tools/stmate-test/` 폴더 내 독립 웹앱의 개발 계획 및 검증 이력을 자체 보관·관리하는 문서입니다.
|
||||
|
||||
## 1. 개요 및 요구사항
|
||||
- **목적**: 경쟁사 데스크톱 프로그램 STMATE(w 8.0, 경영정보(주))의 95% 이상 복제 수준 달성 및 독립 웹앱 구축
|
||||
- **작업 경로**: `tools/stmate-test/`
|
||||
- **구동 서버**: `http://127.0.0.1:8095` (FastAPI + DBF/SMT/STC/JB_R 직결 엔진)
|
||||
- **외부 연동**: 로그인 및 공용 DB 의존 없음, 실무 프로젝트 DBF/SMT/STC 100% 무손실 연동
|
||||
|
||||
---
|
||||
|
||||
## 2. 세부 개발 및 검증 항목
|
||||
|
||||
- [x] **화면 및 메뉴 체계 복제 (TWM_STMATE_START_8X)**
|
||||
- 상단 5대 메뉴(`Act_PrjList`, `Act_Init`, `Act_BoqEdit`, `Act_KanJub`, `Act_KanRate`, `Act_StageEdit`, `Act_Print`)
|
||||
- 8버튼 메인 툴바 및 하단 순공사비·간접비·도급액·총공사비 집계바
|
||||
- 9개 메인 작업 화면:
|
||||
- `wPrjList`: 공사목록표 및 하단 `BOQ11.DBF` 미리보기
|
||||
- `wBoq`: 계층 내역서(`T1..T6`/`E1..E6`/`C*`), 공종번호 자동입력(`wITNum`), 내역할증(`wBoqRate`), 단수보정(`wM_Rate`), 일위풀기(`wExpnd`), 수량산출 연결(`wQtyList`), 내역보관함 (1)(2)(3)
|
||||
- `wM_Set_Sys`: 환경설정 (환율, ECD 소수위, 계산식, 환산식 `1/8*16/12*25/20`)
|
||||
- `wM_JabPum`: 단가목록표 (5대 가격지 비교 `wM_Boxa`, 기준단가 비율조정 `wM_Edt_Hal`, 표준 카탈로그 브라우저)
|
||||
- `wM_Edit_iLWi`: 일위대가·단가산출 수식 편집기 (`COSTN.SMT` 64B 블록 전개식, 표준 카탈로그 894코드·350레시피·21특수산식)
|
||||
- `wM_KanJub`: 제잡비 계산서 (4탭 `T/S/TA/SA`, 16대 콤보박스 및 이윤 끝전조정)
|
||||
- `wM_KAN_RATE`: 원가계산기준 제비율표 (`RATE_R/RA/M/B.DBF` 50×35 및 `ST_BSC/*.JB_R` 파일 직결)
|
||||
- `wStage`: 설계 후속 단계 (`wM_Mk_Cont` 계약내역, `wM_Boq_ExecX` 실행내역, `wM_KanJub_K` 기성내역, `REVISION` 설계변경 증감대비표)
|
||||
- `PM_Prt`: 30종 보고서 인쇄 미리보기 및 6시트 엑셀(`.xls`), 조달청 `<T3_C3B>` XML, JDL 내보내기
|
||||
|
||||
- [x] **100% 범용 `COSTN.SMT` 수식 인터프리터 (`smt_engine.js`)**
|
||||
- 봉화군 2024 `COSTN.SMT` 전수 65/65건(100%) 1원 오차 없이 일치 검증:
|
||||
- `;;` 헤더 변수 분리 파싱
|
||||
- `[A]`, `[D]'%'`, `N'%'` ($N \ge 1 \Rightarrow N \times 0.01$, $N < 1 \Rightarrow N$)
|
||||
- `&PA01:M&`, `&RA01:M&`, `&PS_M&`, `&RC_S:S&` 라벨·구간·누적 소계 처리
|
||||
- `&L00020&` 기구손료 인건비 누적소계 자동참조
|
||||
- `(-=)` 소계제외 블록, `(=)`/`(==)`/`(===)` 구간합산
|
||||
- 운반비(`운반`) 경비(`S`) 일괄 귀속 규칙
|
||||
|
||||
- [x] **5단 연쇄 재계산 엔진 (`cascadeRecalculateAll` / `[계산완료]`)**
|
||||
- 기초단가(`L*`/`M*`/`S*`) 변경 시 실시간 연쇄 전파:
|
||||
1. 중기사용료(`X*` 25/25건 100% 일치, 운전사 노임 좌→우 `1/8*16/12*25/20` 시간당 환산)
|
||||
2. 단가산출(`D*` 65/65건 100% 일치)
|
||||
3. 일위대가(`B*` 45/45건 100% 일치, 공구손료·잡품 `재료비의 %`/`노무비의 %` 구분)
|
||||
4. 내역서(`BOQ11.DBF` 141행)
|
||||
5. 원가계산서(`PRJLST.DBF/.SMT`)
|
||||
|
||||
- [x] **다중 슬롯 DBF 양방향 저장 (`/api/save`)**
|
||||
- 당초설계(`*1`), 설계변경(`*2`), 계약내역(`*3`) 멀티 슬롯 컬럼을 `COSTN.DBF`, `BOQ11.DBF`, `PRJLST.DBF`, `PRJLST.SMT`, `COSTN.SMT` 및 `.STC` 아카이브에 무손실 저장
|
||||
|
||||
- [x] **보고서 엑셀 내보내기 원본 STMATE 1:1 복제 (`excel_builder.py` + `/api/export-xlsx`)**
|
||||
- 원본 STMATE 엑셀 산출물 분석 기반 11개 시트 구성 및 네이티브 하이퍼링크/수식 구현:
|
||||
- `〓 목 차 〓`: 전 시트 바로가기 `=HYPERLINK("#'시트명'!A1", "시트명 →")` 수식 적용
|
||||
- 전 시트 상단 우측: 목차 복귀 `=HYPERLINK("#'〓 목 차 〓'!B2", "목차 →")` 수식 적용
|
||||
- `설계내역서` ↔ `단가산출근거목록표` / `일위대가목록표` 간 셀 참조 수식 및 `=ROUNDDOWN(수량*단가, 0)` 수식화
|
||||
- 원가계산서 비목별 연동 수식 및 11개 시트 스타일링 (헤더 폰트, 테두리, 열 너비 자동 조정)
|
||||
- 백엔드 생성기(`excel_builder.py`, 452줄) + 서버 엔드포인트(`server.py`, 630줄) + UI 연동(`app.js`, 689줄)
|
||||
- 전 파일 700줄 제한 엄격 준수 및 ruff/prettier 포맷팅 적용
|
||||
|
||||
---
|
||||
|
||||
## 3. 검증 결과 기록
|
||||
|
||||
### 자체 검증 (2026-11-05)
|
||||
- **700줄 제한 및 포맷터**: 소스코드 10개 파일 전체 700줄 이하 준수 및 ruff/prettier 포맷 완료.
|
||||
- **브라우저 결함 수정 5건**:
|
||||
1. `wBoq` 상단 헤더 관급자재 합계(`govMatTotal: govRawTotal`) 누락 수정 (`143,554,309원` 표시)
|
||||
2. `wM_KanJub` 및 `PM_Prt` 산정기초 금액 열(`base: baseAmt`) 표시
|
||||
3. `wStage` 설계변경 증감대비표 런타임 에러 수정
|
||||
4. `wM_Edit_iLWi` `D00016` 굴삭기 $Q$ 산식 중간 변수 반올림($f=0.85 \rightarrow Q=22.85$) 적용으로 DB 단가(`3,825원`) 일치
|
||||
5. `wExpnd` 모달 내역서 전개 삽입 연동
|
||||
|
||||
### 자체 검증 (2026-11-07 · 95%+ 완전 복제 고도화)
|
||||
- **Playwright 브라우저 엔진 전수 검증 통과 (`verify_95_fidelity.py`)**:
|
||||
- `COSTN.SMT` 65/65건 100% 일치
|
||||
- 중기사용료 `X*` 25/25건 100% 일치
|
||||
- 단가산출 `D*` 65/65건 100% 일치
|
||||
- 일위대가 `B*` 45/45건 100% 일치
|
||||
- 봉화군 2024 산림유역관리사업 기준:
|
||||
- 직접공사비(`TCOST1`): `781,283,415원`
|
||||
- 도급액(`FINAL_SUPPLY`): `1,270,728,000원`
|
||||
- 관급자재대: `143,555,000원`
|
||||
- 총공사비(`COSTW1`): `1,414,283,000원` (1원 오차 없이 100% 일치)
|
||||
- 동적 연쇄 전파 확인: 경유 `M00018` +100원 인상 시 `X00006` `65,015`→`65,627`원, `D00016` `3,825`→`3,878`원, 총공사비 `1,414,283,000`→`1,423,700,000`원 정상 반영
|
||||
- 6시트 엑셀 워크북(`237,840`바이트) 및 조달청 XML(`52,252`바이트) 정상 생성
|
||||
- 9개 화면 전 과정 콘솔/페이지 런타임 에러 `0건`
|
||||
|
||||
### 자체 검증 (2026-11-07 · 엑셀 원본 STMATE 1:1 하이퍼링크/수식 고도화)
|
||||
- **11개 시트 엑셀 내보내기 검증 완료**:
|
||||
- `〓 목 차 〓`, `공사원가계산서`, `단가산출근거목록표`, `일위대가목록표`, `설계내역서`, `중기사용료`, `단가산출근거`, `일위대가표`, `재료비목록표`, `노무비목록표`, `경비목록표` 총 11개 시트 정상 생성.
|
||||
- 목차의 각 항목별 바로가기 `=HYPERLINK("#'시트명'!A1", "시트명 →")` 및 세부 시트의 목차 복귀 `=HYPERLINK("#'〓 목 차 〓'!B2", "목차 →")` 네이티브 엑셀 하이퍼링크 수식 동작 확인.
|
||||
- 내역서 단가 참조 수식(`=단가산출근거목록표!F{row}`, `=일위대가목록표!F{row}`) 및 산출금액 내림 함수(`=ROUNDDOWN(...)`) 정상 반영.
|
||||
- 서버 상시 구동 상태에서 `POST /api/export-xlsx` 엔드포인트 200 OK 응답 확인.
|
||||
- 전체 소스코드 파일 700줄 이하 준수 (excel_builder.py: 452줄, server.py: 630줄, app.js: 689줄 등) 및 포맷터 완료.
|
||||
+48
-28
@@ -120,24 +120,22 @@
|
||||
}
|
||||
|
||||
function cascadeAndRender() {
|
||||
if (global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll) {
|
||||
if (global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll)
|
||||
global.STMATE_ENGINE.cascadeRecalculateAll(ctx);
|
||||
}
|
||||
renderWorkspace();
|
||||
}
|
||||
|
||||
async function saveAllToDbfServer() {
|
||||
if (!ctx.currentSourcePath || !ctx.dbfData) {
|
||||
if (!ctx.currentSourcePath || !ctx.dbfData)
|
||||
return alert("먼저 PC 또는 실무 STMATE 프로젝트를 로드하세요.");
|
||||
}
|
||||
const stat =
|
||||
global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll
|
||||
? global.STMATE_ENGINE.cascadeRecalculateAll(ctx)
|
||||
: { xCount: 0, dCount: 0, bCount: 0 };
|
||||
recompute();
|
||||
renderChromeBars();
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
const kj = ctx.computed.kanjubComputed,
|
||||
bt = ctx.computed.boqComputed.totals;
|
||||
try {
|
||||
const saved = await postJson("/api/save", ViewPrjBoq.buildSavePayload(ctx));
|
||||
renderWorkspace();
|
||||
@@ -193,7 +191,7 @@
|
||||
const code = ($("expnd-code") || {}).value;
|
||||
const ilwi =
|
||||
(ctx.state.ilwiItems || []).find((x) => x.code === code) || ctx.state.ilwiItems[0];
|
||||
((ilwi && ilwi.lines) || []).forEach((ln, idx) => {
|
||||
((ilwi && ilwi.lines) || []).forEach((ln) => {
|
||||
const r = ctx.state.resources.find((x) => x.id === ln.refId || x.code === ln.refId);
|
||||
insertBoqRow(
|
||||
r ? r.code : ln.refId,
|
||||
@@ -465,10 +463,7 @@
|
||||
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,
|
||||
});
|
||||
ctx.dbfData.costn_memos[code] = Object.assign({}, prev, { text: txt, byteLen: txt.length });
|
||||
}
|
||||
const evalRes = ViewDanIlwi.evaluateSmtFormulaText(txt, ctx, code);
|
||||
if (evalRes.sumT > 0)
|
||||
@@ -500,9 +495,9 @@
|
||||
}),
|
||||
);
|
||||
onClick("kj-btn-apply-jbr", async () => {
|
||||
const jbrPath = $("kj-jbr-select").value;
|
||||
if (!jbrPath) return;
|
||||
const d = await postJson("/api/load-jbr", { jbr_path: jbrPath });
|
||||
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;
|
||||
@@ -513,9 +508,9 @@
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("kr-btn-load-jbr", async () => {
|
||||
const jbrPath = $("kr-jbr-select").value;
|
||||
if (!jbrPath) return;
|
||||
const d = await postJson("/api/load-jbr", { jbr_path: jbrPath });
|
||||
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";
|
||||
@@ -548,14 +543,7 @@
|
||||
ctx.uiState.activeReportSheet = e.target.value;
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("prt-btn-xls", () => {
|
||||
if (!global.STMATE_ENGINE) return;
|
||||
downloadBlob(
|
||||
global.STMATE_ENGINE.buildMultiSheetExcelXml(ctx),
|
||||
`STMATE_Workbook_${ctx.state.projectMeta.pcode || "11"}.xls`,
|
||||
"application/vnd.ms-excel;charset=utf-8",
|
||||
);
|
||||
});
|
||||
onClick("prt-btn-xls", () => exportFullXlsx());
|
||||
onClick("prt-btn-csv", () => {
|
||||
const tbl = document.querySelector("#mdi-workspace table.dbgrid");
|
||||
if (!tbl) return;
|
||||
@@ -584,6 +572,37 @@
|
||||
});
|
||||
}
|
||||
|
||||
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 }));
|
||||
@@ -617,13 +636,14 @@
|
||||
function openMappedModal(key) {
|
||||
const [title, spec] = MENU_MODAL_MAP[key];
|
||||
const [grp, kind] = spec.split(":");
|
||||
const html =
|
||||
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>(SIxMemo 64B 블록 전개식, 10B <code><HII</code> IFORM 포인터) · <code>BOQ11~14.DBF</code> · <code>PRJLST.DBF/.SMT</code> · <code>RATE_R/RA/M.DBF</code> & <code>ST_BSC/*.JB_R</code></p></div>`;
|
||||
openModal(title, html);
|
||||
: `<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) {
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
"""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
|
||||
+41
-21
@@ -9,29 +9,20 @@ import zipfile
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from .smt_parser import (
|
||||
build_dbf_bytes,
|
||||
extract_costn_memos,
|
||||
load_stmate_catalog,
|
||||
parse_dbf_bytes,
|
||||
parse_prjlst_smt,
|
||||
rebuild_costn_with_smt,
|
||||
update_prjlst_smt_bytes,
|
||||
)
|
||||
except ImportError:
|
||||
from smt_parser import (
|
||||
build_dbf_bytes,
|
||||
extract_costn_memos,
|
||||
load_stmate_catalog,
|
||||
parse_dbf_bytes,
|
||||
parse_prjlst_smt,
|
||||
rebuild_costn_with_smt,
|
||||
update_prjlst_smt_bytes,
|
||||
)
|
||||
from excel_builder import build_stmate_full_xlsx
|
||||
from smt_parser import (
|
||||
build_dbf_bytes,
|
||||
extract_costn_memos,
|
||||
load_stmate_catalog,
|
||||
parse_dbf_bytes,
|
||||
parse_prjlst_smt,
|
||||
rebuild_costn_with_smt,
|
||||
update_prjlst_smt_bytes,
|
||||
)
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
REPO_ROOT = HERE.parent.parent
|
||||
@@ -664,9 +655,38 @@ def api_save(req: SaveDbfReq):
|
||||
}
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory=str(HERE), html=True), name="static")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8095)
|
||||
uvicorn.run(app, host="0.0.0.0", port=8095)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,162 @@
|
||||
[START]
|
||||
ARENM=1
|
||||
BMKNM=TLMS
|
||||
ACDNM=1
|
||||
KCDNM=1
|
||||
ZCDNM=2
|
||||
CHGNM=1
|
||||
CHNNM=1
|
||||
BCDNM=2130
|
||||
DCDNM=1497
|
||||
ICDNM=0
|
||||
LCDNM=61
|
||||
MCDNM=1519
|
||||
PCDNM=12
|
||||
QCDNM=0
|
||||
SCDNM=1263
|
||||
WCDNM=2336
|
||||
XCDNM=751
|
||||
SUBNM=
|
||||
LMGNM=20
|
||||
MKRNM=4
|
||||
TMGNM=10
|
||||
JEMNM=123456
|
||||
JMOK1=조달가격
|
||||
JMOK2=물가정보
|
||||
JMOK3=물가자료
|
||||
JMOK4=적산정보
|
||||
JMOK5=견적단가
|
||||
JMOK6=적용 단가
|
||||
JMK10=조달
|
||||
JMK20=물정
|
||||
JMK30=물자
|
||||
JMK40=적산
|
||||
JMK50=견적
|
||||
JUKNM=6
|
||||
YCDNM=0
|
||||
JCDNM=1
|
||||
ECDNM=0
|
||||
BPNNM=1
|
||||
OPTNM=SML
|
||||
CHBNM=1
|
||||
S_JDA=1
|
||||
USRPG=1
|
||||
CUTNM=1
|
||||
|
||||
[PRINT]
|
||||
ORDNM=
|
||||
FONT=굴림체
|
||||
F_COL0=clBlack
|
||||
F_COL1=clRed
|
||||
F_COL2=clBlack
|
||||
SH_TYP0=0
|
||||
SAN_TYP=1
|
||||
BMK_STR1=재 료 비
|
||||
BMK_STR2=노 무 비
|
||||
BMK_STR3=경 비
|
||||
BMK_STR4=합 계
|
||||
EQ_TYP=1
|
||||
SH_TYP1=1
|
||||
SH_TYP2=1
|
||||
SH_TYP3=0
|
||||
FLT_JAB=2
|
||||
CAP_CODE=코드번호
|
||||
CAP_NO=호표
|
||||
CAP_IBRD=분류번호
|
||||
CAP_EQCOD=시설코드
|
||||
CAP_ITNUM=공 종
|
||||
CAP_NAME=명 칭
|
||||
CAP_SIZE=규 격
|
||||
CAP_QTY=수 량
|
||||
CAP_SUNIT=단위
|
||||
CAP_AMT=단 가
|
||||
CAP_PAGE=페이지
|
||||
CAP_AMOUNT=금 액
|
||||
CAP_BIGO=비 고
|
||||
FMT_LEFT=-
|
||||
FMT_RIGHT=-
|
||||
RPOS_PAGE=2
|
||||
RPOS_COP=3
|
||||
RPOS_QTY=2
|
||||
RPOS_SIZE=0
|
||||
RPOS_BIGO=1
|
||||
LNP_CNT=20
|
||||
P_BIGO=1
|
||||
TITL_1LN=0
|
||||
P_SHAMT=0
|
||||
DISP_N1=소계
|
||||
DISP_N2=계
|
||||
DISP_N3=합계
|
||||
DISP_N4=총계
|
||||
DISP_N5=총합계
|
||||
DISP_N6=총대계
|
||||
DISP_EX=제외금액
|
||||
DISP_TOT=총 계
|
||||
DISP_N7=광역계
|
||||
DISP_N8=광역총계
|
||||
DISP_N9=광역대계
|
||||
CH_PRN_COD=0
|
||||
COD_FMT_0=0
|
||||
COD_FMT_1=노무
|
||||
COD_FMT_2=자재
|
||||
COD_FMT_3=경비
|
||||
COD_FMT_4=대가
|
||||
COD_FMT_5=중기
|
||||
COD_FMT_6=산근
|
||||
COD_FMT_7=기타
|
||||
COD_FMT_8=실적
|
||||
EDTNM=
|
||||
CH_PRN_ORD=0
|
||||
CH_MIN_COD=5
|
||||
UPDNCAP=상단:당초 , 하단:변경
|
||||
KAN_TYP=0
|
||||
F_COL3=clBlack
|
||||
|
||||
[EQUIPMENT]
|
||||
RXXNM=1/8*16/12*25/20
|
||||
RXTNM=
|
||||
EXLNM1=1470
|
||||
EXHNM1=1470
|
||||
EXLNM2=1470
|
||||
EXHNM2=1470
|
||||
EXLNM3=1470
|
||||
EXHNM3=1470
|
||||
EXLNM4=1470
|
||||
EXHNM4=1470
|
||||
CONLD=L00038L00039L00040
|
||||
CONTD=
|
||||
CON1D=
|
||||
CON2D=
|
||||
CON3D=
|
||||
CON4D=
|
||||
EXRNM=1
|
||||
RX1NM=1/8*16/12*25/20*24/15
|
||||
RX2NM=1/8*16/12*25/20*12/10
|
||||
RX3NM=1/8*16/12*25/20*24/5
|
||||
RX4NM=
|
||||
EXEUR1=1423
|
||||
EXYEN1=915
|
||||
EXEUR2=1000
|
||||
EXYEN2=1000
|
||||
|
||||
[PROJECT]
|
||||
PCODE=XXXX
|
||||
PMODE=3
|
||||
PNAME=2025년 산불진화임도 신설사업8
|
||||
P_DIR=
|
||||
RGrpTo=1
|
||||
CBoxUpIns=0
|
||||
PRG_V=25.07
|
||||
|
||||
[NAG_HAL]
|
||||
ST_MHAL=0
|
||||
EQ_LHAL=0
|
||||
EQ_MHAL=0
|
||||
EQ_SHAL=0
|
||||
NAGNM=87.745
|
||||
[XML]
|
||||
Company=
|
||||
MkDate=2021-01-12
|
||||
PrjNum=
|
||||
PrjTyp=토목공사
|
||||
PrjBigo=
|
||||
@@ -0,0 +1,162 @@
|
||||
[START]
|
||||
ARENM=1
|
||||
BMKNM=TLMS
|
||||
ACDNM=1
|
||||
KCDNM=1
|
||||
ZCDNM=2
|
||||
CHGNM=1
|
||||
CHNNM=1
|
||||
BCDNM=2130
|
||||
DCDNM=1497
|
||||
ICDNM=0
|
||||
LCDNM=61
|
||||
MCDNM=1519
|
||||
PCDNM=12
|
||||
QCDNM=0
|
||||
SCDNM=1263
|
||||
WCDNM=2336
|
||||
XCDNM=751
|
||||
SUBNM=
|
||||
LMGNM=20
|
||||
MKRNM=4
|
||||
TMGNM=10
|
||||
JEMNM=123456
|
||||
JMOK1=조달가격
|
||||
JMOK2=물가정보
|
||||
JMOK3=물가자료
|
||||
JMOK4=적산정보
|
||||
JMOK5=견적단가
|
||||
JMOK6=적용 단가
|
||||
JMK10=조달
|
||||
JMK20=물정
|
||||
JMK30=물자
|
||||
JMK40=적산
|
||||
JMK50=견적
|
||||
JUKNM=6
|
||||
YCDNM=0
|
||||
JCDNM=1
|
||||
ECDNM=0
|
||||
BPNNM=1
|
||||
OPTNM=SML
|
||||
CHBNM=1
|
||||
S_JDA=1
|
||||
USRPG=1
|
||||
CUTNM=1
|
||||
|
||||
[PRINT]
|
||||
ORDNM=
|
||||
FONT=굴림체
|
||||
F_COL0=clBlack
|
||||
F_COL1=clRed
|
||||
F_COL2=clBlack
|
||||
SH_TYP0=0
|
||||
SAN_TYP=1
|
||||
BMK_STR1=재 료 비
|
||||
BMK_STR2=노 무 비
|
||||
BMK_STR3=경 비
|
||||
BMK_STR4=합 계
|
||||
EQ_TYP=1
|
||||
SH_TYP1=1
|
||||
SH_TYP2=1
|
||||
SH_TYP3=0
|
||||
FLT_JAB=2
|
||||
CAP_CODE=코드번호
|
||||
CAP_NO=호표
|
||||
CAP_IBRD=분류번호
|
||||
CAP_EQCOD=시설코드
|
||||
CAP_ITNUM=공 종
|
||||
CAP_NAME=명 칭
|
||||
CAP_SIZE=규 격
|
||||
CAP_QTY=수 량
|
||||
CAP_SUNIT=단위
|
||||
CAP_AMT=단 가
|
||||
CAP_PAGE=페이지
|
||||
CAP_AMOUNT=금 액
|
||||
CAP_BIGO=비 고
|
||||
FMT_LEFT=-
|
||||
FMT_RIGHT=-
|
||||
RPOS_PAGE=2
|
||||
RPOS_COP=3
|
||||
RPOS_QTY=2
|
||||
RPOS_SIZE=0
|
||||
RPOS_BIGO=1
|
||||
LNP_CNT=20
|
||||
P_BIGO=1
|
||||
TITL_1LN=0
|
||||
P_SHAMT=0
|
||||
DISP_N1=소계
|
||||
DISP_N2=계
|
||||
DISP_N3=합계
|
||||
DISP_N4=총계
|
||||
DISP_N5=총합계
|
||||
DISP_N6=총대계
|
||||
DISP_EX=제외금액
|
||||
DISP_TOT=총 계
|
||||
DISP_N7=광역계
|
||||
DISP_N8=광역총계
|
||||
DISP_N9=광역대계
|
||||
CH_PRN_COD=0
|
||||
COD_FMT_0=0
|
||||
COD_FMT_1=노무
|
||||
COD_FMT_2=자재
|
||||
COD_FMT_3=경비
|
||||
COD_FMT_4=대가
|
||||
COD_FMT_5=중기
|
||||
COD_FMT_6=산근
|
||||
COD_FMT_7=기타
|
||||
COD_FMT_8=실적
|
||||
EDTNM=
|
||||
CH_PRN_ORD=0
|
||||
CH_MIN_COD=5
|
||||
UPDNCAP=상단:당초 , 하단:변경
|
||||
KAN_TYP=0
|
||||
F_COL3=clBlack
|
||||
|
||||
[EQUIPMENT]
|
||||
RXXNM=1/8*16/12*25/20
|
||||
RXTNM=
|
||||
EXLNM1=1470
|
||||
EXHNM1=1470
|
||||
EXLNM2=1470
|
||||
EXHNM2=1470
|
||||
EXLNM3=1470
|
||||
EXHNM3=1470
|
||||
EXLNM4=1470
|
||||
EXHNM4=1470
|
||||
CONLD=L00038L00039L00040
|
||||
CONTD=
|
||||
CON1D=
|
||||
CON2D=
|
||||
CON3D=
|
||||
CON4D=
|
||||
EXRNM=1
|
||||
RX1NM=1/8*16/12*25/20*24/15
|
||||
RX2NM=1/8*16/12*25/20*12/10
|
||||
RX3NM=1/8*16/12*25/20*24/5
|
||||
RX4NM=
|
||||
EXEUR1=1423
|
||||
EXYEN1=915
|
||||
EXEUR2=1000
|
||||
EXYEN2=1000
|
||||
|
||||
[PROJECT]
|
||||
PCODE=XXXX
|
||||
PMODE=3
|
||||
PNAME=2025년 산불진화임도 신설사업8
|
||||
P_DIR=
|
||||
RGrpTo=1
|
||||
CBoxUpIns=0
|
||||
PRG_V=25.07
|
||||
|
||||
[NAG_HAL]
|
||||
ST_MHAL=0
|
||||
EQ_LHAL=0
|
||||
EQ_MHAL=0
|
||||
EQ_SHAL=0
|
||||
NAGNM=87.745
|
||||
[XML]
|
||||
Company=
|
||||
MkDate=2021-01-12
|
||||
PrjNum=
|
||||
PrjTyp=토목공사
|
||||
PrjBigo=
|
||||
Binary file not shown.
Reference in New Issue
Block a user