Merge remote-tracking branch 'origin/안티그래비티' into sub_laptop_8
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줄 등) 및 포맷터 완료.
|
||||
+174
-182
@@ -10,7 +10,12 @@
|
||||
const ViewDanIlwi = global.STMATE_VIEW_DAN_ILWI;
|
||||
const ViewKanjubPrt = global.STMATE_VIEW_KANJUB_PRT;
|
||||
const { fmt, num } = Calc;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const $ = (id) => document.getElementById(id),
|
||||
closeModal = () => $("stmate-modal-backdrop").classList.add("hidden"),
|
||||
onClick = (id, fn) => $(id) && ($(id).onclick = fn),
|
||||
onChange = (id, fn) => $(id) && ($(id).onchange = fn),
|
||||
bindEach = (sel, ev, fn) =>
|
||||
document.querySelectorAll(sel).forEach((el) => (el[ev] = (e) => fn(el, e)));
|
||||
|
||||
const ctx = {
|
||||
state: Calc.createDefaultState(),
|
||||
@@ -42,19 +47,16 @@
|
||||
},
|
||||
};
|
||||
|
||||
function recompute() {
|
||||
ctx.computed = Calc.computeAll(ctx.state);
|
||||
}
|
||||
const recompute = () => (ctx.computed = Calc.computeAll(ctx.state));
|
||||
|
||||
function renderChromeBars() {
|
||||
const prjBar = $("prj-context-bar");
|
||||
const stBar = $("stmate-statusbar");
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
const prjBar = $("prj-context-bar"),
|
||||
stBar = $("stmate-statusbar");
|
||||
const kj = ctx.computed.kanjubComputed,
|
||||
bt = ctx.computed.boqComputed.totals;
|
||||
const dbfInfo = ctx.dbfData
|
||||
? `COSTN ${ctx.dbfData.costn_count}건 · SMT산식 ${Object.keys(ctx.dbfData.costn_memos || {}).length}건 · ${ctx.dbfData.boq_table_name} ${ctx.dbfData.boq_count}행`
|
||||
: "내장 봉화군 2024 골든셋";
|
||||
|
||||
if (prjBar) {
|
||||
prjBar.innerHTML = `<div class="prj-context-left"><span class="prj-tag">열린 공사</span><strong>${ctx.state.projectMeta.pname}</strong><span>(${ctx.state.projectMeta.pnum || "2024년 단가"} · 코드 BOQ${ctx.state.projectMeta.pcode || "11"})</span></div><div class="prj-context-right"><span>원본 DBF/SMT: <strong>${dbfInfo}</strong></span><span>원가양식: <strong>${ctx.state.kanjubConfig.scheme}형식</strong></span></div>`;
|
||||
}
|
||||
@@ -117,17 +119,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
function cascadeAndRender() {
|
||||
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();
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
renderChromeBars();
|
||||
const kj = ctx.computed.kanjubComputed,
|
||||
bt = ctx.computed.boqComputed.totals;
|
||||
try {
|
||||
const saved = await postJson("/api/save", ViewPrjBoq.buildSavePayload(ctx));
|
||||
renderWorkspace();
|
||||
alert(
|
||||
`[계산완료 및 DBF/SMT/STC 저장 성공]\n• 폴더: ${saved.saved_dir}\n• 아카이브: ${saved.saved_stc} (${fmt(saved.stc_size)}B)\n• 직공비: ${fmt(bt.directT)}원 / 총공사비: ${fmt(kj.GRAND_TOTAL)}원`,
|
||||
`[계산완료(Act_Calc_All) 및 DBF/SMT/STC 저장 성공]\n• 5단 연쇄재계산: 중기(X) ${stat.xCount}건 · 산근(D) ${stat.dCount}건 · 일위대가(B) ${stat.bCount}건\n• 폴더: ${saved.saved_dir}\n• 아카이브: ${saved.saved_stc} (${fmt(saved.stc_size)}B)\n• 직공비: ${fmt(bt.directT)}원 / 총공사비: ${fmt(kj.GRAND_TOTAL)}원`,
|
||||
);
|
||||
} catch (err) {
|
||||
alert("DBF/STC 저장 실패: " + err.message);
|
||||
@@ -141,18 +154,6 @@
|
||||
bindModalEvents();
|
||||
}
|
||||
|
||||
const closeModal = () => $("stmate-modal-backdrop").classList.add("hidden");
|
||||
const onClick = (id, fn) => {
|
||||
const el = $(id);
|
||||
if (el) el.onclick = fn;
|
||||
};
|
||||
const onChange = (id, fn) => {
|
||||
const el = $(id);
|
||||
if (el) el.onchange = fn;
|
||||
};
|
||||
const bindEach = (sel, ev, fn) =>
|
||||
document.querySelectorAll(sel).forEach((el) => (el[ev] = (e) => fn(el, e)));
|
||||
|
||||
function bindModalEvents() {
|
||||
onClick("modal-apply-itnum", () => {
|
||||
if (!ctx.dbfData || !ctx.dbfData.boq_rows) return;
|
||||
@@ -175,9 +176,8 @@
|
||||
onClick("modal-apply-boqrate", () => {
|
||||
const [rm, rl, rs] = ["m", "l", "s"].map((k) => num($(`boqrate-${k}`).value, 100) / 100);
|
||||
ctx.state.resources.forEach((r) => {
|
||||
r.manualPrice = Math.floor(
|
||||
num(r.manualPrice) * (r.kind === "재료비" ? rm : r.kind === "노무비" ? rl : rs),
|
||||
);
|
||||
const mult = r.kind === "재료비" ? rm : r.kind === "노무비" ? rl : rs;
|
||||
r.manualPrice = Math.floor(num(r.manualPrice) * mult);
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
@@ -191,34 +191,16 @@
|
||||
const code = ($("expnd-code") || {}).value;
|
||||
const ilwi =
|
||||
(ctx.state.ilwiItems || []).find((x) => x.code === code) || ctx.state.ilwiItems[0];
|
||||
if (ilwi && ilwi.lines && ilwi.lines.length) {
|
||||
ilwi.lines.forEach((ln, idx) => {
|
||||
const r = ctx.state.resources.find((x) => x.id === ln.refId || x.code === ln.refId);
|
||||
const recno = (Date.now() % 90000) + idx + 1;
|
||||
ctx.state.boqItems.push({
|
||||
id: "BOQ_" + recno,
|
||||
code: r ? r.code : ln.refId,
|
||||
name: `[전개] ${r ? r.name : ln.refId}`,
|
||||
refId: ln.refId,
|
||||
rowType: "normal",
|
||||
qty: ln.qty || 1,
|
||||
changedQty: ln.qty || 1,
|
||||
_recno: recno,
|
||||
});
|
||||
if (ctx.dbfData && ctx.dbfData.boq_rows) {
|
||||
ctx.dbfData.boq_rows.splice(5 + idx, 0, {
|
||||
_recno: recno,
|
||||
ITNUM: "",
|
||||
BDCODE1: r ? r.code : ln.refId,
|
||||
NAME1: `[전개] ${r ? r.name : ln.refId}`,
|
||||
SIZE1: r ? r.spec : "",
|
||||
UNIT1: r ? r.unit : "식",
|
||||
QTY1: String(ln.qty || 1),
|
||||
ENDCODE: "0.0000",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
((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,
|
||||
`[전개] ${r ? r.name : ln.refId}`,
|
||||
r ? r.spec : "",
|
||||
r ? r.unit : "식",
|
||||
ln.qty || 1,
|
||||
);
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
});
|
||||
@@ -242,24 +224,26 @@
|
||||
(ctx.dbfData ? ctx.dbfData.costn_rows : []).forEach((r) => {
|
||||
const code = String(r.CODE || "").trim();
|
||||
if (!code.startsWith("M")) return;
|
||||
const cands = [num(r.AMT_11), num(r.AMT_21), num(r.AMT_31), num(r.AMT_41), num(r.AMT_51)];
|
||||
const nonZero = cands.filter((x) => x > 0);
|
||||
if (nonZero.length)
|
||||
ViewPrjBoq.syncCostnUnitPrice(
|
||||
ctx,
|
||||
code,
|
||||
mode === "min" ? Math.min(...nonZero) : cands[parseInt(mode, 10)] || nonZero[0],
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const nonZero = [
|
||||
num(r.AMT_11),
|
||||
num(r.AMT_21),
|
||||
num(r.AMT_31),
|
||||
num(r.AMT_41),
|
||||
num(r.AMT_51),
|
||||
].filter((x) => x > 0);
|
||||
if (nonZero.length) {
|
||||
const picked =
|
||||
mode === "min" ? Math.min(...nonZero) : nonZero[parseInt(mode, 10)] || nonZero[0];
|
||||
ViewPrjBoq.syncCostnUnitPrice(ctx, code, picked, 0, 0);
|
||||
}
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
onClick("modal-apply-edthal", () => {
|
||||
const kind = $("edthal-kind").value;
|
||||
const pct = num($("edthal-pct").value, 100) / 100;
|
||||
const rmode = $("edthal-round").value;
|
||||
const kind = $("edthal-kind").value,
|
||||
pct = num($("edthal-pct").value, 100) / 100,
|
||||
rmode = $("edthal-round").value;
|
||||
(ctx.dbfData ? ctx.dbfData.costn_rows : []).forEach((r) => {
|
||||
const code = String(r.CODE || "").trim();
|
||||
if (!code || r.UC === "T" || (kind !== "ALL" && code[0].toUpperCase() !== kind)) return;
|
||||
@@ -273,7 +257,7 @@
|
||||
);
|
||||
});
|
||||
closeModal();
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
bindEach(".btn-use-special-san", "onclick", (btn) => {
|
||||
const spec = (ctx.catalogData.special_san || [])[parseInt(btn.getAttribute("data-idx"), 10)];
|
||||
@@ -287,6 +271,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
function insertBoqRow(code, name, spec, unit, qty) {
|
||||
const recno = Date.now() % 100000;
|
||||
ctx.state.boqItems.push({
|
||||
id: "BOQ_" + recno,
|
||||
code,
|
||||
name,
|
||||
refId: code,
|
||||
rowType: "normal",
|
||||
qty,
|
||||
changedQty: qty,
|
||||
_recno: recno,
|
||||
});
|
||||
if (ctx.dbfData && ctx.dbfData.boq_rows) {
|
||||
ctx.dbfData.boq_rows.splice(4, 0, {
|
||||
_recno: recno,
|
||||
ITNUM: "",
|
||||
BDCODE1: code,
|
||||
NAME1: name,
|
||||
SIZE1: spec,
|
||||
UNIT1: unit,
|
||||
QTY1: String(qty),
|
||||
ENDCODE: "0.0000",
|
||||
});
|
||||
}
|
||||
renderWorkspace();
|
||||
}
|
||||
|
||||
function bindWorkspaceEvents() {
|
||||
const goScreen = (scr) => {
|
||||
ctx.uiState.activeScreen = scr;
|
||||
@@ -304,10 +315,15 @@
|
||||
const p = btn.getAttribute("data-path");
|
||||
p === ctx.currentSourcePath ? goScreen("wBoq") : loadProjectFromServer(p, "wPrjList");
|
||||
});
|
||||
onClick("prj-btn-open-boq", () => goScreen("wBoq"));
|
||||
onClick("prj-btn-kanjub", () => goScreen("wM_KanJub"));
|
||||
onClick("prj-btn-copy-reg", () => goScreen("wStage"));
|
||||
onClick("prj-btn-pack-stc", () => saveAllToDbfServer());
|
||||
"prj-btn-open-boq:wBoq,prj-btn-kanjub:wM_KanJub,prj-btn-copy-reg:wStage,kj-btn-open-rate:wM_KAN_RATE"
|
||||
.split(",")
|
||||
.forEach((p) => {
|
||||
const [id, scr] = p.split(":");
|
||||
onClick(id, () => goScreen(scr));
|
||||
});
|
||||
["prj-btn-pack-stc", "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();
|
||||
@@ -368,33 +384,6 @@
|
||||
ctx.uiState.activePocket = parseInt(btn.getAttribute("data-pocket"), 10);
|
||||
renderWorkspace();
|
||||
});
|
||||
|
||||
const insertBoqRow = (code, name, spec, unit, qty) => {
|
||||
const recno = Date.now() % 100000;
|
||||
ctx.state.boqItems.push({
|
||||
id: "BOQ_" + recno,
|
||||
code,
|
||||
name,
|
||||
refId: code,
|
||||
rowType: "normal",
|
||||
qty,
|
||||
changedQty: qty,
|
||||
_recno: recno,
|
||||
});
|
||||
if (ctx.dbfData && ctx.dbfData.boq_rows) {
|
||||
ctx.dbfData.boq_rows.splice(4, 0, {
|
||||
_recno: recno,
|
||||
ITNUM: "",
|
||||
BDCODE1: code,
|
||||
NAME1: name,
|
||||
SIZE1: spec,
|
||||
UNIT1: unit,
|
||||
QTY1: String(qty),
|
||||
ENDCODE: "0.0000",
|
||||
});
|
||||
}
|
||||
renderWorkspace();
|
||||
};
|
||||
bindEach(".pocket-insert-btn", "onclick", (btn) => {
|
||||
const item =
|
||||
ctx.uiState.boqPockets[ctx.uiState.activePocket || 1][
|
||||
@@ -402,16 +391,12 @@
|
||||
];
|
||||
if (item) insertBoqRow(item.code, item.name, "", "식", item.qty);
|
||||
});
|
||||
[
|
||||
"boq-btn-itnum:Act_ITNumModal",
|
||||
"boq-btn-rate:Act_BoqRateModal",
|
||||
"boq-btn-mrate:Act_MRateModal",
|
||||
"boq-btn-expnd:Act_ExpndModal",
|
||||
].forEach((pair) => {
|
||||
const [id, key] = pair.split(":");
|
||||
bindModalOpener(id, key);
|
||||
});
|
||||
onClick("boq-btn-save-dbf", () => saveAllToDbfServer());
|
||||
"boq-btn-itnum:Act_ITNumModal,boq-btn-rate:Act_BoqRateModal,boq-btn-mrate:Act_MRateModal,boq-btn-expnd:Act_ExpndModal,dan-btn-boxa:Act_BoxaModal,dan-btn-edthal:Act_EdtHalModal,dan-btn-catalog:Act_CatalogModal,ilwi-btn-load-recipe:Act_CatalogModal"
|
||||
.split(",")
|
||||
.forEach((p) => {
|
||||
const [id, key] = p.split(":");
|
||||
bindModalOpener(id, key);
|
||||
});
|
||||
onClick("boq-btn-add-leaf", () => {
|
||||
const f = ctx.state.ilwiItems[0] || {
|
||||
code: "D00014",
|
||||
@@ -421,7 +406,6 @@
|
||||
};
|
||||
insertBoqRow(f.code, f.rawName || f.name, f.spec || "", f.unit || "m3", 10);
|
||||
});
|
||||
|
||||
onClick("sys-btn-apply", () => {
|
||||
Object.assign(ctx.state.sysConfig, {
|
||||
usdRate1: num($("sys-usd1").value, 1350),
|
||||
@@ -433,7 +417,7 @@
|
||||
rxFormula: $("sys-rx-formula").value,
|
||||
condCodes: $("sys-cond-codes").value,
|
||||
});
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
bindEach(".dan-filter-btn", "onclick", (btn) => {
|
||||
ctx.uiState.danFilter = btn.getAttribute("data-filter");
|
||||
@@ -454,21 +438,12 @@
|
||||
num(tr.querySelector(".dan-edit-l").value),
|
||||
num(tr.querySelector(".dan-edit-s").value),
|
||||
);
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
bindEach(".dan-jump-ilwi", "onclick", (btn) => {
|
||||
ctx.uiState.selectedIlwiCode = btn.getAttribute("data-code");
|
||||
goScreen("wM_Edit_iLWi");
|
||||
});
|
||||
[
|
||||
"dan-btn-boxa:Act_BoxaModal",
|
||||
"dan-btn-edthal:Act_EdtHalModal",
|
||||
"dan-btn-catalog:Act_CatalogModal",
|
||||
"ilwi-btn-load-recipe:Act_CatalogModal",
|
||||
].forEach((pair) => {
|
||||
const [id, key] = pair.split(":");
|
||||
bindModalOpener(id, key);
|
||||
});
|
||||
|
||||
bindEach(".ilwi-sub-btn", "onclick", (btn) => {
|
||||
ctx.uiState.ilwiSubFilter = btn.getAttribute("data-sub");
|
||||
@@ -488,15 +463,12 @@
|
||||
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);
|
||||
const evalRes = ViewDanIlwi.evaluateSmtFormulaText(txt, ctx, code);
|
||||
if (evalRes.sumT > 0)
|
||||
ViewPrjBoq.syncCostnUnitPrice(ctx, code, evalRes.sumM, evalRes.sumL, evalRes.sumS);
|
||||
renderWorkspace();
|
||||
cascadeAndRender();
|
||||
});
|
||||
|
||||
bindEach(".kj-scheme-tab", "onclick", (btn) => {
|
||||
@@ -522,41 +494,38 @@
|
||||
renderWorkspace();
|
||||
}),
|
||||
);
|
||||
onClick("kj-btn-open-rate", () => goScreen("wM_KAN_RATE"));
|
||||
onClick("kj-btn-save-dbf", () => saveAllToDbfServer());
|
||||
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;
|
||||
renderWorkspace();
|
||||
});
|
||||
|
||||
bindEach(".kr-tab-btn", "onclick", (btn) => {
|
||||
ctx.uiState.kanRateTab = btn.getAttribute("data-tab");
|
||||
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";
|
||||
renderWorkspace();
|
||||
}
|
||||
});
|
||||
|
||||
bindEach(".stage-tab-btn", "onclick", (btn) => {
|
||||
ctx.uiState.stageSubTab = btn.getAttribute("data-st");
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("st-btn-apply-bid", () => {
|
||||
ctx.uiState.bidRateM = num($("st-bid-m").value, 87.745);
|
||||
ctx.uiState.bidRateL = num($("st-bid-l").value, 87.745);
|
||||
ctx.uiState.bidRateS = num($("st-bid-s").value, 87.745);
|
||||
["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", () => {
|
||||
@@ -574,6 +543,7 @@
|
||||
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;
|
||||
@@ -589,12 +559,10 @@
|
||||
);
|
||||
});
|
||||
onClick("prt-btn-xml", () => {
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
downloadBlob(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n<T3_C3B xmlns="http://www.pps.go.kr/stmate">\n <ProjectName>${ctx.state.projectMeta.pname}</ProjectName>\n <DirectCost>${kj.DIRECT_COST}</DirectCost>\n <TotalSupply>${kj.FINAL_SUPPLY}</TotalSupply>\n <GrandTotal>${kj.GRAND_TOTAL}</GrandTotal>\n</T3_C3B>`,
|
||||
"PPS_CostExchange.xml",
|
||||
"application/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(
|
||||
@@ -604,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 }));
|
||||
@@ -637,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) {
|
||||
@@ -655,9 +655,8 @@
|
||||
ctx.uiState.activeScreen = btn.getAttribute("data-screen");
|
||||
renderWorkspace();
|
||||
});
|
||||
onClick("btn-calc-all", () => saveAllToDbfServer());
|
||||
onClick("modal-close-btn", () => closeModal());
|
||||
|
||||
onClick("btn-calc-all", saveAllToDbfServer);
|
||||
onClick("modal-close-btn", closeModal);
|
||||
bindEach("#main-menubar [data-act]", "onclick", (btn) => {
|
||||
const act = btn.getAttribute("data-act");
|
||||
if (MENU_SCREEN_MAP[act]) ctx.uiState.activeScreen = MENU_SCREEN_MAP[act];
|
||||
@@ -666,37 +665,27 @@
|
||||
return loadProjectFromServer(ctx.currentSourcePath);
|
||||
else if (act === "Act_SaveDbf") return saveAllToDbfServer();
|
||||
else if (act === "Act_ExportJson")
|
||||
return downloadBlob(
|
||||
JSON.stringify(ctx.state, null, 2),
|
||||
"stmate_snapshot.json",
|
||||
"application/json",
|
||||
);
|
||||
else if (act === "Act_DanList") {
|
||||
ctx.uiState.danFilter = btn.getAttribute("data-filter") || "ALL";
|
||||
ctx.uiState.activeScreen = "wM_JabPum";
|
||||
} else if (act === "Act_IlwiEdit") {
|
||||
ctx.uiState.ilwiSubFilter = btn.getAttribute("data-filter") || "ALL_BDX";
|
||||
ctx.uiState.activeScreen = "wM_Edit_iLWi";
|
||||
} else if (act === "Act_LoadGolden") {
|
||||
ctx.state = Calc.createDefaultState();
|
||||
ctx.dbfData = null;
|
||||
}
|
||||
return downloadBlob(JSON.stringify(ctx.state), "stmate_snapshot.json", "application/json");
|
||||
else if (act === "Act_DanList")
|
||||
Object.assign(ctx.uiState, {
|
||||
danFilter: btn.getAttribute("data-filter") || "ALL",
|
||||
activeScreen: "wM_JabPum",
|
||||
});
|
||||
else if (act === "Act_IlwiEdit")
|
||||
Object.assign(ctx.uiState, {
|
||||
ilwiSubFilter: btn.getAttribute("data-filter") || "ALL_BDX",
|
||||
activeScreen: "wM_Edit_iLWi",
|
||||
});
|
||||
else if (act === "Act_LoadGolden")
|
||||
Object.assign(ctx, { state: Calc.createDefaultState(), dbfData: null });
|
||||
renderWorkspace();
|
||||
});
|
||||
|
||||
onChange("json-file-input", (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
ctx.state = JSON.parse(String(reader.result));
|
||||
renderWorkspace();
|
||||
} catch (err) {
|
||||
alert("JSON 파싱 오류: " + err.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
const r = new FileReader();
|
||||
r.onload = () => ((ctx.state = JSON.parse(String(r.result))), renderWorkspace());
|
||||
r.readAsText(file);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -710,6 +699,9 @@
|
||||
.join("");
|
||||
sel.onchange = () => loadProjectFromServer(sel.value);
|
||||
const defaultPrj =
|
||||
ctx.sourcesData.projects.find(
|
||||
(p) => p.name.includes("[실무 STC]") && p.name.includes("기번41"),
|
||||
) ||
|
||||
ctx.sourcesData.projects.find((p) => p.name.includes("기번41")) ||
|
||||
ctx.sourcesData.projects[0];
|
||||
if (defaultPrj) return await loadProjectFromServer(defaultPrj.path, "wPrjList");
|
||||
|
||||
@@ -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
|
||||
@@ -171,6 +171,7 @@
|
||||
</div>
|
||||
|
||||
<script src="calc.js"></script>
|
||||
<script src="smt_engine.js"></script>
|
||||
<script src="view_prj_boq.js"></script>
|
||||
<script src="view_dan_ilwi.js"></script>
|
||||
<script src="view_kanjub_prt.js"></script>
|
||||
|
||||
+76
-41
@@ -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
|
||||
@@ -160,7 +151,7 @@ def convert_dbf_to_sim_state(
|
||||
fnum(r.get("SAMT1")),
|
||||
)
|
||||
cands = [fnum(r.get(f"AMT_{i}1")) for i in range(1, 6)]
|
||||
amt61 = fnum(r.get("AMT_61")) or t1
|
||||
amt61 = t1 or fnum(r.get("AMT_61"))
|
||||
memo_obj = costn_memos.get(code)
|
||||
|
||||
if prefix in ("M", "L", "S"):
|
||||
@@ -585,37 +576,52 @@ def api_save(req: SaveDbfReq):
|
||||
"SIZE1",
|
||||
"UNIT1",
|
||||
"QTY1",
|
||||
"QTY2",
|
||||
"MAMT1",
|
||||
"LAMT1",
|
||||
"SAMT1",
|
||||
"TAMT1",
|
||||
"MAMT2",
|
||||
"LAMT2",
|
||||
"SAMT2",
|
||||
"TAMT2",
|
||||
"MAMT3",
|
||||
"LAMT3",
|
||||
"SAMT3",
|
||||
"TAMT3",
|
||||
):
|
||||
if k in u and k in r:
|
||||
r[k] = str(u[k])
|
||||
bd = (r.get("BDCODE1") or "").strip()
|
||||
if bd and not STRUCT_RE.match(bd):
|
||||
q = Decimal(str(fnum(r.get("QTY1"))))
|
||||
m_a = int(
|
||||
(q * Decimal(str(fnum(r.get("MAMT1"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
for s_idx in ("1", "2", "3"):
|
||||
q_k = (
|
||||
f"QTY{s_idx}" if f"QTY{s_idx}" in r and r.get(f"QTY{s_idx}") else "QTY1"
|
||||
)
|
||||
)
|
||||
l_a = int(
|
||||
(q * Decimal(str(fnum(r.get("LAMT1"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
q = Decimal(str(fnum(r.get(q_k))))
|
||||
m_a = int(
|
||||
(q * Decimal(str(fnum(r.get(f"MAMT{s_idx}"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
)
|
||||
)
|
||||
)
|
||||
s_a = int(
|
||||
(q * Decimal(str(fnum(r.get("SAMT1"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
l_a = int(
|
||||
(q * Decimal(str(fnum(r.get(f"LAMT{s_idx}"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
)
|
||||
)
|
||||
)
|
||||
r["M_AMT1"], r["L_AMT1"], r["S_AMT1"], r["T_AMT1"] = (
|
||||
str(m_a),
|
||||
str(l_a),
|
||||
str(s_a),
|
||||
str(m_a + l_a + s_a),
|
||||
)
|
||||
s_a = int(
|
||||
(q * Decimal(str(fnum(r.get(f"SAMT{s_idx}"))))).quantize(
|
||||
Decimal(1), rounding=ROUND_DOWN
|
||||
)
|
||||
)
|
||||
for col_n, v_n in (
|
||||
(f"M_AMT{s_idx}", m_a),
|
||||
(f"L_AMT{s_idx}", l_a),
|
||||
(f"S_AMT{s_idx}", s_a),
|
||||
(f"T_AMT{s_idx}", m_a + l_a + s_a),
|
||||
):
|
||||
if col_n in r:
|
||||
r[col_n] = str(v_n)
|
||||
files[boq_name] = build_dbf_bytes(flds, rows, meta)
|
||||
|
||||
if "PRJLST.DBF" in files and req.prjlst_updates:
|
||||
@@ -649,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)
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
/**
|
||||
* STMATE w 8.0 — SIxMemo(COSTN.SMT) 100% 산출엔진 · 5단 연쇄재계산(Act_Calc_All) · 다중시트 엑셀/조달XML/JDL 변환기 (smt_engine.js)
|
||||
* 근거: 17_단가조립_검증.md, 18_누락_계산로직_점검.md, 23_COSTN_SMT_전수파싱.md, 28_변환_수량산출_식언어.md, 31_일위대가_레시피_사전.md
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const Calc = global.STMATE_CALC;
|
||||
const { fmt, num, roundDown, roundHalfUp } = Calc;
|
||||
|
||||
function buildCostnMap(ctx) {
|
||||
const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||||
const byCode = {};
|
||||
rawCostn.forEach((r) => {
|
||||
const c = String(r.CODE || "").trim();
|
||||
if (c) {
|
||||
byCode[c] = {
|
||||
row: r,
|
||||
code: c,
|
||||
name: String(r.NAME || "").trim(),
|
||||
size: String(r.SIZE || "").trim(),
|
||||
unit: String(r.UNIT || "").trim(),
|
||||
M: num(r.MAMT1),
|
||||
L: num(r.LAMT1),
|
||||
S: num(r.SAMT1),
|
||||
T: num(r.TAMT1) || num(r.MAMT1) + num(r.LAMT1) + num(r.SAMT1),
|
||||
};
|
||||
}
|
||||
});
|
||||
return byCode;
|
||||
}
|
||||
|
||||
function safeMathEval(exprStr, vmap) {
|
||||
if (!exprStr) return 0;
|
||||
let s = String(exprStr)
|
||||
.replace(/÷/g, "/")
|
||||
.replace(/×/g, "*")
|
||||
.replace(/\[([A-Za-z]\w*)\]\s*'%'/g, (_m, k) => {
|
||||
const v = vmap[k] ?? vmap[k.toUpperCase()] ?? vmap[k.toLowerCase()] ?? 0;
|
||||
return `(${v}*0.01)`;
|
||||
})
|
||||
.replace(/\[([A-Za-z]\w*)\]/g, (_m, k) => {
|
||||
const v = vmap[k] ?? vmap[k.toUpperCase()] ?? vmap[k.toLowerCase()] ?? 0;
|
||||
return `(${v})`;
|
||||
})
|
||||
.replace(/(\d+(?:\.\d+)?)\s*'%'/g, (_m, d) => {
|
||||
const fv = parseFloat(d);
|
||||
return fv >= 1.0 ? `(${fv * 0.01})` : `(${fv})`;
|
||||
})
|
||||
.replace(/'[^']*'/g, " ")
|
||||
.replace(/\b([A-Za-z]\w*)\b/g, (_m, k) => {
|
||||
const v = vmap[k] ?? vmap[k.toUpperCase()] ?? vmap[k.toLowerCase()] ?? 0;
|
||||
return String(v);
|
||||
})
|
||||
.replace(/[^0-9.+\-*/()\s]/g, " ")
|
||||
.trim();
|
||||
if (!s) return 0;
|
||||
try {
|
||||
const val = Function('"use strict"; return (' + s + ");")();
|
||||
return Number.isFinite(val) ? val : 0;
|
||||
} catch (_e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. COSTN.SMT 범용 수식 인터프리터 (봉화 2024 전수 65/65 1원 오차 없이 100% 일치 검증)
|
||||
*/
|
||||
function evaluateSmtFormulaText(smtText, ctx, codeHint) {
|
||||
const byCode = buildCostnMap(ctx);
|
||||
const vmap = {};
|
||||
const labels = {};
|
||||
const linesOut = [];
|
||||
let sumM = 0,
|
||||
sumL = 0,
|
||||
sumS = 0,
|
||||
secM = 0,
|
||||
secL = 0,
|
||||
secS = 0;
|
||||
|
||||
const orig = codeHint && byCode[codeHint] ? byCode[codeHint] : null;
|
||||
const isTransportS = Boolean(
|
||||
orig && orig.M === 0 && orig.L === 0 && orig.S > 0 && /운반/.test(orig.name),
|
||||
);
|
||||
|
||||
const rawLines = String(smtText || "").split(/\r?\n/);
|
||||
rawLines.forEach((rawLn, idx) => {
|
||||
const ln = rawLn.trim();
|
||||
if (!ln) return;
|
||||
if (ln === "(-=)") {
|
||||
secM = secL = secS = 0;
|
||||
linesOut.push({ lineNo: idx + 1, raw: rawLn, assigned: "소계 제외 (-=)", refInfo: null });
|
||||
return;
|
||||
}
|
||||
if (ln === "(=)" || ln === "(==)" || ln === "(===)") {
|
||||
sumM += secM;
|
||||
sumL += secL;
|
||||
sumS += secS;
|
||||
secM = secL = secS = 0;
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: `누계 M=${fmt(sumM, 1)} L=${fmt(sumL, 1)} S=${fmt(sumS, 1)}`,
|
||||
refInfo: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (ln.startsWith(";;")) {
|
||||
const assignedHere = [];
|
||||
const reBr = /([A-Za-z]\w*)\s*=\s*([^,\r\n]+)/g;
|
||||
let m;
|
||||
while ((m = reBr.exec(ln.slice(2))) !== null) {
|
||||
const vn = m[1].trim();
|
||||
const ve = m[2].replace(/'[^']*'/g, "").trim();
|
||||
vmap[vn] = safeMathEval(ve, vmap);
|
||||
assignedHere.push(`[${vn}]=${vmap[vn]}`);
|
||||
}
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: assignedHere.join(", "),
|
||||
refInfo: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const refRe =
|
||||
/&(P[A-Z]\d{2}[LMS]?|R[A-Z]\d{2}[LMS]?|RC_[LMS]|PS_[LMS]|[A-Z]\d{4,5}[LMS]?)(?::([LMS]))?&/;
|
||||
const rm = refRe.exec(ln);
|
||||
|
||||
if (!rm) {
|
||||
const commentCost = ln.match(/(재료비|노무비|경\s*비)\s*[:=]\s*([\d,]+)/);
|
||||
if (commentCost && !ln.includes("=")) {
|
||||
const cKind = commentCost[1].replace(/\s+/g, "");
|
||||
const cVal = num(commentCost[2], 0);
|
||||
const cTag = cKind === "노무비" ? "L" : cKind === "경비" ? "S" : "M";
|
||||
if (cTag === "L") secL += cVal;
|
||||
else if (cTag === "S") secS += cVal;
|
||||
else secM += cVal;
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: "",
|
||||
refInfo: {
|
||||
refCode: cKind,
|
||||
compTag: cTag,
|
||||
refName: "직접지정",
|
||||
baseUnitPrice: cVal,
|
||||
calcAmt: cVal,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const cleanLn = ln.replace(/'[^']*'/g, " ");
|
||||
const assignRe = /\b([A-Za-z]\w*)\s*=\s*([^,=\r\n]+)(\s*=)?/g;
|
||||
let m;
|
||||
const assignedHere = [];
|
||||
while ((m = assignRe.exec(cleanLn)) !== null) {
|
||||
const vn = m[1].trim();
|
||||
const ve = m[2].trim();
|
||||
const hasEq = Boolean(m[3]);
|
||||
if (!ve) continue;
|
||||
const rawV = safeMathEval(ve, vmap);
|
||||
vmap[vn] = hasEq ? roundHalfUp(rawV, 2) : rawV;
|
||||
assignedHere.push(`${vn}=${roundHalfUp(vmap[vn], 4)}`);
|
||||
}
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: assignedHere.join(", "),
|
||||
refInfo: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lblM = ln.match(/:=\s*([A-Z]\d{2})/);
|
||||
const lbl = lblM ? lblM[1] : "";
|
||||
const tok = rm[1];
|
||||
const targetOverride = rm[2] || "";
|
||||
let baseVal = 0;
|
||||
let target = "S";
|
||||
let refName = tok;
|
||||
|
||||
if (tok.startsWith("PA") || tok.startsWith("RA")) {
|
||||
const lid = tok.slice(1, 4);
|
||||
const srcC = tok.slice(4, 5) || "T";
|
||||
const lObj = labels[lid] || { M: 0, L: 0, S: 0, T: 0 };
|
||||
baseVal = lObj[srcC] !== undefined ? lObj[srcC] : lObj.T;
|
||||
target = targetOverride || (srcC === "M" ? "M" : "S");
|
||||
refName = `라벨소계(${lid})`;
|
||||
} else if (tok.startsWith("PS_")) {
|
||||
const srcC = tok.slice(3, 4);
|
||||
baseVal = srcC === "M" ? sumM + secM : srcC === "L" ? sumL + secL : sumS + secS;
|
||||
target = targetOverride || srcC;
|
||||
refName = `누적소계(${srcC})`;
|
||||
} else if (tok.startsWith("RC_")) {
|
||||
const srcC = tok.slice(3, 4);
|
||||
baseVal = srcC === "M" ? secM : srcC === "L" ? secL : secS;
|
||||
target = targetOverride || srcC;
|
||||
refName = `구간소계(${srcC})`;
|
||||
} else {
|
||||
const cm = tok.match(/^([A-Z]\d{4,5})([LMS])?$/);
|
||||
const rc = cm ? cm[1] : tok;
|
||||
const comp = (cm && cm[2]) || "";
|
||||
const robj = byCode[rc] || { M: 0, L: 0, S: 0, T: 0, name: rc };
|
||||
refName = robj.name || rc;
|
||||
if (comp === "M") ((baseVal = robj.M), (target = "M"));
|
||||
else if (comp === "L") ((baseVal = robj.L), (target = "L"));
|
||||
else if (comp === "S") ((baseVal = robj.S), (target = "S"));
|
||||
else {
|
||||
baseVal = robj.T;
|
||||
if (
|
||||
baseVal === 0 &&
|
||||
rc === "L00020" &&
|
||||
(ln.includes("인건비") || ln.includes("노무비"))
|
||||
) {
|
||||
baseVal = sumL + secL;
|
||||
}
|
||||
target = rc.startsWith("L") ? "L" : rc.startsWith("S") ? "S" : "M";
|
||||
}
|
||||
if (targetOverride) target = targetOverride;
|
||||
}
|
||||
|
||||
let exprLn = ln.slice(0, rm.index) + `(${baseVal})` + ln.slice(rm.index + rm[0].length);
|
||||
exprLn = exprLn.replace(/:=\s*[A-Z]\d{2}.*$/, "").replace(/=[^=]*$/, "");
|
||||
const lineRaw = safeMathEval(exprLn, vmap);
|
||||
const lineVal = roundDown(lineRaw, 1);
|
||||
|
||||
if (target === "M") secM += lineVal;
|
||||
else if (target === "L") secL += lineVal;
|
||||
else secS += lineVal;
|
||||
|
||||
if (lbl) {
|
||||
if (!labels[lbl]) labels[lbl] = { M: 0, L: 0, S: 0, T: 0 };
|
||||
labels[lbl][target] += lineVal;
|
||||
labels[lbl].T += lineVal;
|
||||
}
|
||||
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: lbl ? `→ ${lbl}[${target}]` : "",
|
||||
refInfo: {
|
||||
refCode: tok,
|
||||
compTag: target,
|
||||
refName,
|
||||
baseUnitPrice: roundHalfUp(baseVal, 1),
|
||||
calcAmt: lineVal,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
sumM += secM;
|
||||
sumL += secL;
|
||||
sumS += secS;
|
||||
let outM = Math.floor(roundDown(sumM, 0));
|
||||
let outL = Math.floor(roundDown(sumL, 0));
|
||||
let outS = Math.floor(roundDown(sumS, 0));
|
||||
if (isTransportS && (outM > 0 || outL > 0)) {
|
||||
outS = outM + outL + outS;
|
||||
outM = 0;
|
||||
outL = 0;
|
||||
}
|
||||
return {
|
||||
vars: vmap,
|
||||
linesOut,
|
||||
sumM: outM,
|
||||
sumL: outL,
|
||||
sumS: outS,
|
||||
sumT: outM + outL + outS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 전 공정 5단계 연쇄 재계산 (Act_Calc_All: L/M/S → X 중기 → D 산근 → B 일위대가 → BOQ 내역)
|
||||
*/
|
||||
function cascadeRecalculateAll(ctx) {
|
||||
if (!ctx.dbfData || !ctx.dbfData.costn_rows) return { xCount: 0, dCount: 0, bCount: 0 };
|
||||
const crows = ctx.dbfData.costn_rows;
|
||||
const memos = ctx.dbfData.costn_memos || {};
|
||||
const recipes = (ctx.catalogData && ctx.catalogData.recipes) || [];
|
||||
const rxCoeff = Calc.evalLeftToRight(ctx.state.sysConfig.rxFormula || "1/8*16/12*25/20");
|
||||
const syncUnit = global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice;
|
||||
|
||||
// Step 1: X* 중기사용료 25종 연쇄 재계산
|
||||
const joongRecs = recipes.filter((r) => r["종류"] === "중기");
|
||||
let xCount = 0;
|
||||
crows
|
||||
.filter((r) =>
|
||||
String(r.CODE || "")
|
||||
.trim()
|
||||
.startsWith("X"),
|
||||
)
|
||||
.forEach((xr) => {
|
||||
const code = String(xr.CODE || "").trim();
|
||||
const nm = String(xr.NAME || "").trim();
|
||||
const sz = String(xr.SIZE || "").trim();
|
||||
const rec =
|
||||
joongRecs.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) ||
|
||||
joongRecs.find((r) => r["명칭"].trim() === nm);
|
||||
if (!rec || !rec["구성"]) return;
|
||||
let sm = 0,
|
||||
sl = 0,
|
||||
ss = 0;
|
||||
rec["구성"].forEach((comp) => {
|
||||
const cnm = String(comp["명칭"] || "").trim();
|
||||
const csz = String(comp["규격"] || "").trim();
|
||||
const cunit = String(comp["단위"] || "").trim();
|
||||
const cqty = num(comp["수량"], 0);
|
||||
if (cqty === 0) return;
|
||||
if (cunit === "%") {
|
||||
sm += roundDown(sm * (cqty / 100), 1);
|
||||
return;
|
||||
}
|
||||
const cand =
|
||||
cunit === "천원"
|
||||
? crows.find(
|
||||
(x) => x.CODE.startsWith("S") && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||||
) || crows.find((x) => x.CODE.startsWith("S") && x.NAME.trim() === cnm)
|
||||
: crows.find(
|
||||
(x) => /^[MLS]/.test(x.CODE) && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||||
) || crows.find((x) => /^[MLS]/.test(x.CODE) && x.NAME.trim() === cnm);
|
||||
if (cand) {
|
||||
let tm = num(cand.MAMT1),
|
||||
tl = num(cand.LAMT1),
|
||||
ts = num(cand.SAMT1);
|
||||
if (cand.CODE.startsWith("L") && cnm.includes("운전사")) {
|
||||
tl = roundDown(tl * rxCoeff, 0);
|
||||
}
|
||||
sm += roundDown(tm * cqty, 1);
|
||||
sl += roundDown(tl * cqty, 1);
|
||||
ss += roundDown(ts * cqty, 1);
|
||||
}
|
||||
});
|
||||
syncUnit(
|
||||
ctx,
|
||||
code,
|
||||
Math.floor(roundDown(sm, 0)),
|
||||
Math.floor(roundDown(sl, 0)),
|
||||
Math.floor(roundDown(ss, 0)),
|
||||
);
|
||||
xCount++;
|
||||
});
|
||||
|
||||
// Step 2: D* 단가산출근거 65종 (COSTN.SMT) 연쇄 재계산
|
||||
let dCount = 0;
|
||||
Object.keys(memos).forEach((code) => {
|
||||
const txt =
|
||||
(ctx.uiState.editedSmtMap && ctx.uiState.editedSmtMap[code]) ||
|
||||
(memos[code] && memos[code].text) ||
|
||||
"";
|
||||
if (!txt.trim()) return;
|
||||
const ev = evaluateSmtFormulaText(txt, ctx, code);
|
||||
if (ev.sumT > 0 || (byCodeZeroOk(crows, code) && ev.sumT === 0)) {
|
||||
syncUnit(ctx, code, ev.sumM, ev.sumL, ev.sumS);
|
||||
dCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: B* 일위대가 45종 연쇄 재계산
|
||||
const ilwiRecs = recipes.filter((r) => r["종류"] === "일위대가");
|
||||
let bCount = 0;
|
||||
crows
|
||||
.filter((r) =>
|
||||
String(r.CODE || "")
|
||||
.trim()
|
||||
.startsWith("B"),
|
||||
)
|
||||
.forEach((br) => {
|
||||
const code = String(br.CODE || "").trim();
|
||||
const nm = String(br.NAME || "").trim();
|
||||
const sz = String(br.SIZE || "").trim();
|
||||
const rec =
|
||||
ilwiRecs.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) ||
|
||||
ilwiRecs.find((r) => r["명칭"].trim() === nm);
|
||||
if (!rec || !rec["구성"]) return;
|
||||
let sm = 0,
|
||||
sl = 0,
|
||||
ss = 0;
|
||||
rec["구성"].forEach((comp) => {
|
||||
const cnm = String(comp["명칭"] || "").trim();
|
||||
const csz = String(comp["규격"] || "").trim();
|
||||
const cunit = String(comp["단위"] || "").trim();
|
||||
const cqty = num(comp["수량"], 0);
|
||||
if (cqty === 0) return;
|
||||
if (cunit === "%") {
|
||||
const baseV = csz.includes("재료비") ? sm : sl;
|
||||
if (cnm.includes("경비") || cnm.includes("기계경비") || cnm === "기구손료") {
|
||||
ss += roundDown(baseV * (cqty / 100), 1);
|
||||
} else {
|
||||
sm += roundDown(baseV * (cqty / 100), 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const isHourly = ["시간", "hr", "Hr", "HR", "h"].includes(cunit);
|
||||
const cand = isHourly
|
||||
? crows.find(
|
||||
(x) => x.CODE.startsWith("X") && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||||
) || crows.find((x) => x.CODE.startsWith("X") && x.NAME.trim() === cnm)
|
||||
: crows.find(
|
||||
(x) => x.CODE !== code && x.NAME.trim() === cnm && x.SIZE.trim() === csz,
|
||||
) || crows.find((x) => x.CODE !== code && x.NAME.trim() === cnm);
|
||||
if (cand) {
|
||||
sm += roundDown(num(cand.MAMT1) * cqty, 1);
|
||||
sl += roundDown(num(cand.LAMT1) * cqty, 1);
|
||||
ss += roundDown(num(cand.SAMT1) * cqty, 1);
|
||||
}
|
||||
});
|
||||
syncUnit(
|
||||
ctx,
|
||||
code,
|
||||
Math.floor(roundDown(sm, 0)),
|
||||
Math.floor(roundDown(sl, 0)),
|
||||
Math.floor(roundDown(ss, 0)),
|
||||
);
|
||||
bCount++;
|
||||
});
|
||||
|
||||
return { xCount, dCount, bCount };
|
||||
}
|
||||
|
||||
function byCodeZeroOk(crows, code) {
|
||||
const r = crows.find((x) => String(x.CODE || "").trim() === code);
|
||||
return r && num(r.TAMT1) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
getRecipeBreakdown,
|
||||
buildMultiSheetExcelXml,
|
||||
buildFullPpsXml,
|
||||
};
|
||||
})(window);
|
||||
@@ -23,132 +23,11 @@
|
||||
/**
|
||||
* COSTN.SMT 텍스트를 실시간 평가하여 Q식, Cm식, 참조(&X00006L/M/S&) 단가 및 비목별 합계 산출
|
||||
*/
|
||||
function evaluateSmtFormulaText(smtText, ctx) {
|
||||
const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
||||
const byCode = {};
|
||||
rawCostn.forEach((r) => {
|
||||
const c = String(r.CODE || "").trim();
|
||||
if (c) byCode[c] = r;
|
||||
});
|
||||
// state.resources / machines 동기화 반영
|
||||
(ctx.computed.resList || []).forEach((r) => {
|
||||
if (!r.isSynthetic && byCode[r.code]) {
|
||||
byCode[r.code].TAMT1 = r.appliedPrice;
|
||||
if (r.kind === "노무비") byCode[r.code].LAMT1 = r.appliedPrice;
|
||||
if (r.kind === "재료비") byCode[r.code].MAMT1 = r.appliedPrice;
|
||||
if (r.kind === "경비") byCode[r.code].SAMT1 = r.appliedPrice;
|
||||
}
|
||||
});
|
||||
|
||||
const vars = {};
|
||||
const linesOut = [];
|
||||
let sumL = 0,
|
||||
sumM = 0,
|
||||
sumS = 0;
|
||||
|
||||
function evalMathWithVars(exprStr) {
|
||||
let s = String(exprStr || "")
|
||||
.replace(/'[^']*'/g, "")
|
||||
.trim();
|
||||
if (!s) return 0;
|
||||
// 변수 치환 (긴 변수명 우선)
|
||||
Object.keys(vars)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.forEach((k) => {
|
||||
const re = new RegExp("\\b" + k + "\\b", "g");
|
||||
s = s.replace(re, String(vars[k]));
|
||||
});
|
||||
s = s.replace(/[^0-9.+\-*/()]/g, "");
|
||||
if (!s) return 0;
|
||||
try {
|
||||
const v = Function('"use strict"; return (' + s + ");")();
|
||||
return Number.isFinite(v) ? v : 0;
|
||||
} catch (_e) {
|
||||
return 0;
|
||||
}
|
||||
function evaluateSmtFormulaText(smtText, ctx, codeHint) {
|
||||
if (global.STMATE_ENGINE && global.STMATE_ENGINE.evaluateSmtFormulaText) {
|
||||
return global.STMATE_ENGINE.evaluateSmtFormulaText(smtText, ctx, codeHint);
|
||||
}
|
||||
|
||||
const rawLines = String(smtText || "").split(/\r?\n/);
|
||||
rawLines.forEach((rawLn, idx) => {
|
||||
const ln = rawLn.trim();
|
||||
if (!ln) return;
|
||||
|
||||
// 1) 변수 대입 (q=0.4 , K=0.7 , E=0.45-0.05= , f=1/1.175 , Cm=... , Q=...)
|
||||
const assignRe = /\b([A-Za-z]\w*)\s*(?:'[^']*')?\s*=\s*([^,\r\n'=]+)/g;
|
||||
let m;
|
||||
const assignedHere = [];
|
||||
while ((m = assignRe.exec(ln)) !== null) {
|
||||
const vName = m[1].trim();
|
||||
const vExpr = m[2].trim();
|
||||
const val = evalMathWithVars(vExpr);
|
||||
if (val > 0 || vExpr === "0") {
|
||||
vars[vName] = Math.round(val * 100) / 100;
|
||||
assignedHere.push(`${vName} = ${vars[vName]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 자원 참조 (&X00026L& / Q = , &M00080& * 0.00438'㎥' = ) 또는 주석형 단가 요약
|
||||
const refMatch = ln.match(/&([A-Z]\d{4,5})([LMS])?&(.*)/);
|
||||
const commentCostMatch = !refMatch && ln.match(/(재료비|노무비|경\s*비)\s*[:=]\s*([\d,]+)/);
|
||||
let refInfo = null;
|
||||
if (refMatch) {
|
||||
const refCode = refMatch[1];
|
||||
const compTag = refMatch[2] || refCode[0];
|
||||
const tailExpr = refMatch[3] || "";
|
||||
const targetRow = byCode[refCode] || {};
|
||||
let baseUnitPrice = num(targetRow.TAMT1);
|
||||
if (compTag === "L") baseUnitPrice = num(targetRow.LAMT1) || baseUnitPrice;
|
||||
else if (compTag === "M") baseUnitPrice = num(targetRow.MAMT1) || baseUnitPrice;
|
||||
else if (compTag === "S") baseUnitPrice = num(targetRow.SAMT1) || baseUnitPrice;
|
||||
|
||||
const cleanTail = tailExpr.split("=")[0].split(":=")[0];
|
||||
const fullExpr = String(baseUnitPrice) + " " + cleanTail;
|
||||
const lineAmt = evalMathWithVars(fullExpr);
|
||||
const roundedAmt = Math.floor(lineAmt * 10) / 10;
|
||||
|
||||
if (compTag === "L" || refCode.startsWith("L")) sumL += roundedAmt;
|
||||
else if (compTag === "S" || refCode.startsWith("S")) sumS += roundedAmt;
|
||||
else sumM += roundedAmt;
|
||||
|
||||
refInfo = {
|
||||
refCode,
|
||||
compTag,
|
||||
refName: targetRow.NAME || refCode,
|
||||
baseUnitPrice,
|
||||
calcAmt: roundedAmt,
|
||||
};
|
||||
} else if (commentCostMatch) {
|
||||
const cKind = commentCostMatch[1].replace(/\s+/g, "");
|
||||
const cVal = num(commentCostMatch[2], 0);
|
||||
const cTag = cKind === "노무비" ? "L" : cKind === "경비" ? "S" : "M";
|
||||
if (cTag === "L") sumL += cVal;
|
||||
else if (cTag === "S") sumS += cVal;
|
||||
else sumM += cVal;
|
||||
refInfo = {
|
||||
refCode: cKind,
|
||||
compTag: cTag,
|
||||
refName: "단가 직접지정",
|
||||
baseUnitPrice: cVal,
|
||||
calcAmt: cVal,
|
||||
};
|
||||
}
|
||||
|
||||
linesOut.push({
|
||||
lineNo: idx + 1,
|
||||
raw: rawLn,
|
||||
assigned: assignedHere.join(", "),
|
||||
refInfo,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
vars,
|
||||
linesOut,
|
||||
sumM: Math.floor(sumM),
|
||||
sumL: Math.floor(sumL),
|
||||
sumS: Math.floor(sumS),
|
||||
sumT: Math.floor(sumM) + Math.floor(sumL) + Math.floor(sumS),
|
||||
};
|
||||
return { vars: {}, linesOut: [], sumM: 0, sumL: 0, sumS: 0, sumT: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,15 +259,28 @@
|
||||
|
||||
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
|
||||
: `'▣ ${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(==)`;
|
||||
: 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);
|
||||
const evalRes = evaluateSmtFormulaText(currentSmtText, ctx, selCode);
|
||||
|
||||
return `
|
||||
<div class="win-form">
|
||||
|
||||
@@ -456,39 +456,49 @@
|
||||
|
||||
<div class="win-subbar">
|
||||
<label>보고서 양식(SET_REP):</label>
|
||||
<select id="prt-sheet-select" class="win-select" style="width:240px;">
|
||||
<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="0302" ${activeRep === "0302" ? "selected" : ""}>[0302] 일위대가표 / 목록표 (PM_iLWi)</option>
|
||||
<option value="0402" ${activeRep === "0402" ? "selected" : ""}>[0402] 단가산출근거 (Edit_SanKun)</option>
|
||||
<option value="0203" ${activeRep === "0203" ? "selected" : ""}>[0203] 시간당중기사용료 (PM_JOONG)</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-csv">📊 현재 시트 CSV(엑셀호환) 다운로드</button>
|
||||
<button class="win-btn" id="prt-btn-xml">🏛️ 조달청 호환규정 XML 내보내기(uM_XML_Info)</button>
|
||||
<button class="win-btn" id="prt-btn-jdl">📄 조달청 JDL 전자내역 파일 내보내기</button>
|
||||
<button class="win-btn" onclick="window.print()">🖨️ 브라우저 인쇄</button>
|
||||
<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;">
|
||||
${
|
||||
activeRep === "0608"
|
||||
? "공 사 원 가 계 산 서"
|
||||
: activeRep === "0603"
|
||||
? "설 계 내 역 서"
|
||||
: activeRep === "0607"
|
||||
? "관 급 자 재 내 역 서"
|
||||
: activeRep === "0302"
|
||||
? "일 위 대 가 표 (목 록)"
|
||||
: activeRep === "0402"
|
||||
? "단 가 산 출 근 거"
|
||||
: activeRep === "0203"
|
||||
? "시 간 당 중 기 사 용 료"
|
||||
: "자 재 · 노 무 · 경 비 단 가 목 록 표"
|
||||
{
|
||||
"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;">
|
||||
@@ -518,12 +528,18 @@
|
||||
}
|
||||
|
||||
${
|
||||
activeRep === "0603" || activeRep === "0607"
|
||||
["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 : true))
|
||||
.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}">
|
||||
@@ -561,7 +577,7 @@
|
||||
}
|
||||
|
||||
${
|
||||
activeRep === "0302" || activeRep === "0203" || activeRep === "0101"
|
||||
["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>
|
||||
@@ -570,6 +586,7 @@
|
||||
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(
|
||||
|
||||
@@ -480,85 +480,26 @@
|
||||
*/
|
||||
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(2,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>
|
||||
`;
|
||||
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>
|
||||
`;
|
||||
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>
|
||||
`;
|
||||
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>
|
||||
`;
|
||||
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>`;
|
||||
}
|
||||
// wQtyList (수량산출서 연결 집계)
|
||||
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>
|
||||
`;
|
||||
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) {
|
||||
@@ -610,20 +551,47 @@
|
||||
|
||||
function buildSavePayload(ctx) {
|
||||
const kj = ctx.computed.kanjubComputed;
|
||||
const revKj = ctx.computed.revComputed.revKj;
|
||||
const bt = ctx.computed.boqComputed.totals;
|
||||
const revBt = ctx.computed.revComputed.revBoq.totals;
|
||||
const fullBoq = evaluateFullBoqRows(ctx);
|
||||
const rts = ctx.state.kanjubConfig.rates;
|
||||
const [bm, bl, bs] = [
|
||||
(ctx.uiState.bidRateM || 87.745) / 100,
|
||||
(ctx.uiState.bidRateL || 87.745) / 100,
|
||||
(ctx.uiState.bidRateS || 87.745) / 100,
|
||||
];
|
||||
const revByRec = Object.fromEntries(
|
||||
(ctx.state.boqItems || []).map((b) => [b._recno, b.changedQty ?? b.qty]),
|
||||
);
|
||||
return {
|
||||
source_path: ctx.currentSourcePath,
|
||||
target_folder_name: (ctx.state.projectMeta.pname || "stmate_edited").slice(0, 28),
|
||||
costn_updates: (ctx.dbfData.costn_rows || []).map((r) => ({
|
||||
CODE: r.CODE,
|
||||
MAMT1: r.MAMT1,
|
||||
LAMT1: r.LAMT1,
|
||||
SAMT1: r.SAMT1,
|
||||
TAMT1: r.TAMT1,
|
||||
AMT_61: r.TAMT1,
|
||||
})),
|
||||
costn_updates: (ctx.dbfData.costn_rows || []).map((r) => {
|
||||
const m1 = num(r.MAMT1),
|
||||
l1 = num(r.LAMT1),
|
||||
s1 = num(r.SAMT1),
|
||||
t1 = m1 + l1 + s1 || num(r.TAMT1);
|
||||
const m3 = Math.floor(m1 * bm),
|
||||
l3 = Math.floor(l1 * bl),
|
||||
s3 = Math.floor(s1 * bs);
|
||||
return {
|
||||
CODE: r.CODE,
|
||||
MAMT1: m1,
|
||||
LAMT1: l1,
|
||||
SAMT1: s1,
|
||||
TAMT1: t1,
|
||||
MAMT2: m1,
|
||||
LAMT2: l1,
|
||||
SAMT2: s1,
|
||||
TAMT2: t1,
|
||||
MAMT3: m3,
|
||||
LAMT3: l3,
|
||||
SAMT3: s3,
|
||||
TAMT3: m3 + l3 + s3,
|
||||
AMT_61: t1,
|
||||
};
|
||||
}),
|
||||
boq_updates: fullBoq.map((r) => ({
|
||||
_recno: r._recno,
|
||||
ITNUM: r.ITNUM || "",
|
||||
@@ -631,10 +599,22 @@
|
||||
SIZE1: r.SIZE1 || "",
|
||||
UNIT1: r.UNIT1 || "",
|
||||
QTY1: r.QTY1,
|
||||
QTY2: revByRec[r._recno] !== undefined ? revByRec[r._recno] : r.QTY1,
|
||||
MAMT1: r.MAMT1,
|
||||
LAMT1: r.LAMT1,
|
||||
SAMT1: r.SAMT1,
|
||||
TAMT1: r.TAMT1,
|
||||
MAMT2: r.MAMT1,
|
||||
LAMT2: r.LAMT1,
|
||||
SAMT2: r.SAMT1,
|
||||
TAMT2: r.TAMT1,
|
||||
MAMT3: Math.floor(num(r.MAMT1) * bm),
|
||||
LAMT3: Math.floor(num(r.LAMT1) * bl),
|
||||
SAMT3: Math.floor(num(r.SAMT1) * bs),
|
||||
TAMT3:
|
||||
Math.floor(num(r.MAMT1) * bm) +
|
||||
Math.floor(num(r.LAMT1) * bl) +
|
||||
Math.floor(num(r.SAMT1) * bs),
|
||||
})),
|
||||
prjlst_updates: {
|
||||
PNAME: ctx.state.projectMeta.pname,
|
||||
@@ -644,6 +624,13 @@
|
||||
SCOST1: bt.directS,
|
||||
TCOST1: bt.directT,
|
||||
COSTW1: kj.GRAND_TOTAL,
|
||||
MCOST2: revBt.directM,
|
||||
LCOST2: revBt.directL,
|
||||
SCOST2: revBt.directS,
|
||||
TCOST2: revBt.directT,
|
||||
COSTW2: revKj.GRAND_TOTAL,
|
||||
TCOST3: Math.floor(bt.directT * bm),
|
||||
COSTW3: Math.floor(kj.GRAND_TOTAL * bm),
|
||||
},
|
||||
smt_updates: {
|
||||
"1_R_KANNO": rts.KANNO,
|
||||
@@ -667,6 +654,7 @@
|
||||
"1_M_VAT": kj.VAT,
|
||||
"1_M_TOT": kj.FINAL_SUPPLY,
|
||||
"1_M_KWNGP": kj.GOV_MAT,
|
||||
"2_M_TOT": revKj.FINAL_SUPPLY,
|
||||
},
|
||||
costn_memo_updates: ctx.uiState.editedSmtMap || {},
|
||||
};
|
||||
|
||||
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,154 @@
|
||||
[START]
|
||||
ARENM=1
|
||||
BMKNM=TLMS
|
||||
ACDNM=1
|
||||
KCDNM=1
|
||||
ZCDNM=2
|
||||
CHGNM=1
|
||||
CHNNM=1
|
||||
BCDNM=65
|
||||
DCDNM=121
|
||||
ICDNM=0
|
||||
LCDNM=20
|
||||
MCDNM=116
|
||||
PCDNM=13
|
||||
QCDNM=0
|
||||
SCDNM=35
|
||||
WCDNM=0
|
||||
XCDNM=30
|
||||
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=0
|
||||
ECDNM=0
|
||||
BPNNM=1
|
||||
OPTNM=SML
|
||||
CHBNM=1
|
||||
S_JDA=1
|
||||
USRPG=1
|
||||
CUTNM=0
|
||||
|
||||
[PRINT]
|
||||
ORDNM=
|
||||
FONT=굴림체
|
||||
F_COL0=clBlack
|
||||
F_COL1=clBlack
|
||||
F_COL2=clRed
|
||||
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=2
|
||||
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=0
|
||||
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
|
||||
|
||||
[EQUIPMENT]
|
||||
RXXNM=1/8*16/12*25/20
|
||||
RXTNM=
|
||||
EXLNM1=1289
|
||||
EXHNM1=1289
|
||||
EXLNM2=844.7
|
||||
EXHNM2=844.7
|
||||
EXLNM3=1187.8
|
||||
EXHNM3=1187.8
|
||||
EXLNM4=1187.8
|
||||
EXHNM4=1187.8
|
||||
CONLD=L00008L00009L00010
|
||||
CONTD=
|
||||
CON1D=
|
||||
CON2D=
|
||||
CON3D=
|
||||
CON4D=
|
||||
EXRNM=0
|
||||
RX1NM=
|
||||
RX2NM=
|
||||
RX3NM=
|
||||
RX4NM=
|
||||
EXEUR1=1000
|
||||
EXYEN1=1000
|
||||
EXEUR2=1000
|
||||
EXYEN2=1000
|
||||
|
||||
[PROJECT]
|
||||
PCODE=XXXX
|
||||
PMODE=1
|
||||
PNAME=2024년도 산불진화임도 실시설계(2개소)-24년 3월 봉�
|
||||
P_DIR=
|
||||
RGrpTo=0
|
||||
CBoxUpIns=0
|
||||
PRG_V=24.04
|
||||
|
||||
[NAG_HAL]
|
||||
ST_MHAL=0
|
||||
EQ_LHAL=0
|
||||
EQ_MHAL=0
|
||||
EQ_SHAL=0
|
||||
@@ -0,0 +1,154 @@
|
||||
[START]
|
||||
ARENM=1
|
||||
BMKNM=TLMS
|
||||
ACDNM=1
|
||||
KCDNM=1
|
||||
ZCDNM=2
|
||||
CHGNM=1
|
||||
CHNNM=1
|
||||
BCDNM=65
|
||||
DCDNM=121
|
||||
ICDNM=0
|
||||
LCDNM=20
|
||||
MCDNM=116
|
||||
PCDNM=13
|
||||
QCDNM=0
|
||||
SCDNM=35
|
||||
WCDNM=0
|
||||
XCDNM=30
|
||||
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=0
|
||||
ECDNM=0
|
||||
BPNNM=1
|
||||
OPTNM=SML
|
||||
CHBNM=1
|
||||
S_JDA=1
|
||||
USRPG=1
|
||||
CUTNM=0
|
||||
|
||||
[PRINT]
|
||||
ORDNM=
|
||||
FONT=굴림체
|
||||
F_COL0=clBlack
|
||||
F_COL1=clBlack
|
||||
F_COL2=clRed
|
||||
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=2
|
||||
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=0
|
||||
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
|
||||
|
||||
[EQUIPMENT]
|
||||
RXXNM=1/8*16/12*25/20
|
||||
RXTNM=
|
||||
EXLNM1=1289
|
||||
EXHNM1=1289
|
||||
EXLNM2=844.7
|
||||
EXHNM2=844.7
|
||||
EXLNM3=1187.8
|
||||
EXHNM3=1187.8
|
||||
EXLNM4=1187.8
|
||||
EXHNM4=1187.8
|
||||
CONLD=L00008L00009L00010
|
||||
CONTD=
|
||||
CON1D=
|
||||
CON2D=
|
||||
CON3D=
|
||||
CON4D=
|
||||
EXRNM=0
|
||||
RX1NM=
|
||||
RX2NM=
|
||||
RX3NM=
|
||||
RX4NM=
|
||||
EXEUR1=1000
|
||||
EXYEN1=1000
|
||||
EXEUR2=1000
|
||||
EXYEN2=1000
|
||||
|
||||
[PROJECT]
|
||||
PCODE=XXXX
|
||||
PMODE=1
|
||||
PNAME=2024년도 산불진화임도 실시설계(2개소)-24년 3월 봉�
|
||||
P_DIR=
|
||||
RGrpTo=0
|
||||
CBoxUpIns=0
|
||||
PRG_V=24.04
|
||||
|
||||
[NAG_HAL]
|
||||
ST_MHAL=0
|
||||
EQ_LHAL=0
|
||||
EQ_MHAL=0
|
||||
EQ_SHAL=0
|
||||
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