knowledge(경쟁사): STmate 원가계산 골든셋 회귀 — 프로젝트 6건 원 단위 재현 성공

프로젝트 STC 의 BOQ11 만 입력으로 삼아 직접비부터 총공사비까지 다시 계산하고
STmate 가 뽑은 XLSX 공사원가계산서와 원 단위로 대조한 결과.

결과
- 실무 프로젝트 6건 전부, 모든 항목이 1원도 안 틀림
- 내역 행 금액 ROUNDDOWN(수량 x 단가) 대조 1,515건 중 불일치 0
- 봉화 2024 는 직접비부터 총공사비 1,414,283,000 까지 전 항목 일치

새로 밝힌 규칙 다섯 — 이것 없이는 맞지 않음
- 말단 행 판별: BDCODE1 접두 T*(제목)·E*(계)·C*(제외총계)·:(페이지) 가 구조 행
- C* 로 닫히는 구역은 제외금액 구역이라 직접비에서 뺌
- 산업안전보건관리비의 관급재 밑수 = 관급자재대 - 조달수수료
- 끝전조정 = 도급공사비 천원 차액을 ROUND(차액/1.1) 로 세전 환산해 이윤에서 차감
- 관급자재대 = ROUNDUP(원자재대, 천원)

이로써 09번 완료 정의의 원설계 재현 조건이 실증으로 충족됨.
끝전조정·간접비 순서·계층 집계·제외금액 취급이 B 에서 A 실증으로 올라감.

문서 16 신규, 회귀 스크립트 2개 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Prk9BCHG1EMAywk9k8wegA
This commit is contained in:
2026-09-13 12:17:51 +09:00
co-authored by Claude Opus 5
parent b656e5944d
commit 87d4519ce0
6 changed files with 393 additions and 2 deletions
@@ -22,6 +22,7 @@
| 13 통제시험 목록 | 정적 분석으로 못 가르는 경계를 시험 11개로 분리, 우선순위·완료판정 | https://share.onorca.dev/a/lcmxdrG2SWvE |
| 14 최종 분석완료 보고서 | 도달 수준, A등급 확정 목록, 남은 것, 완료 판정 기준, 산출물 | https://share.onorca.dev/a/AmgldP7q_Qzc |
| 15 재구현 가능성 검증 | **무엇을 만들 수 있고 무엇이 막혀 있나** — 층별 확보표, 문서 오류 정정, T1 좌표 확정 | https://share.onorca.dev/a/t6HGy0HlbEBl |
| 16 골든셋 회귀 결과 | **원가계산을 다시 세워 원 단위로 맞춘 실증** — 프로젝트 6건 전 항목 일치 | (저장소 파일) |
> **정본은 저장소 파일이다** — `resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/`.
> 위 Orca 링크는 보기용이며, 문서를 지식DB 자리로 옮기면서 주소가 한 번 바뀌었다.
@@ -49,6 +50,10 @@
| `TAMT=MAMT+LAMT+SAMT` | 예외 없는 불변조건 | **예외 있음** — 59행 중 2행(관급 흄관) | 11번 §3, 15번 §3 |
| 프로젝트 RATE 표 | 4종 | **6종**`RATE_A`·`RATE_B` 추가 발견 | 02번, 15번 §3 |
| 보험 요율 좌표 | 미확정 | **A** — 산재·건강·연금·장기요양·고용 등급1~7·산안비 좌표 확정 | 12번 §9.1, 15번 §4 |
| 원가계산 전 과정 | 규칙 서술 | **A 실증** — 프로젝트 6건을 원 단위로 다시 세워 전 항목 일치 | 16번 |
| 내역 말단 행 판별 | 없음 | **A**`BDCODE1` 접두 `T/E/C/:` 가 구조 행 | 16번 §4.1 |
| 제외금액 구역 | 서술만 | **A**`C*` 로 닫히는 구역은 직접비에서 뺌 | 16번 §4.2 |
| 끝전조정 | B | **A 실증** — 차액 → `ROUND(차액/1.1)` → 이윤 차감 | 16번 §4.4 |
분석일: 2026-09-13
대상: `C:\경영정보(주)`
@@ -136,7 +136,7 @@ A등급 불변조건으로 적혀 있으나 실데이터에 **59행 중 2행이
| 물음 | 답 |
|---|---|
| STmate와 **똑같은 프로그램**을 만들 수 있나 | ❌ — 기초단가 원천이 암호화됨 |
| STmate와 **같은 답을 내는 계산 엔진**을 만들 수 있나 | ⚠ 대부분 — 직접비·간접비·반올림은 가능, 중기·할증·자재채택은 불가 |
| STmate와 **같은 답을 내는 계산 엔진**을 만들 수 있나 | ✅ **원가계산 부분은 실증 완료** (16번, 프로젝트 6건 원 단위 일치). 단가 조립·할증·자재채택은 여전히 불가 |
| STmate 프로젝트 파일을 **읽고 검산**할 수 있나 | ✅ — `BDQTY` 빼고 전부 |
| **Aislo 원가계산에 쓸 규칙**을 얻었나 | ✅ — 밑수·순서·반올림·요율 좌표 확보 |
@@ -146,7 +146,7 @@ A등급 불변조건으로 적혀 있으나 실데이터에 **59행 중 2행이
| 순위 | 할 일 | 왜 |
|---:|---|---|
| 1 | **골든셋 회귀 구현** — 실무 7건의 STC + XLSX로 내역·간접비를 원 단위 대조 | 지금 자료만으로 되고, 규칙 확정도를 한 번에 끌어올림 |
| ~~1~~ | ~~골든셋 회귀 구현~~ | ✅ **완료 — 16번.** 프로젝트 6건 전 항목 원 단위 일치 |
| 2 | 남은 요율 좌표(연금·일반관리비·이윤·환경보전비) 확정 | 통제시험 T1 나머지 |
| 3 | 중기 손료 계수를 **법령·품셈에서** 확보 | STmate 자료로는 영구 불가 — 국가 기준으로 대체 |
| 4 | `BDQTY` 대조시험 (T7) | 할증 규칙 |
@@ -0,0 +1,139 @@
# 골든셋 회귀 결과 — 원가계산을 원 단위로 다시 세워 맞췄다
실행일: 2026-09-13
방법: 프로젝트 STC의 `BOQ11`만 입력으로 삼아 **직접비 → 간접비 → 이윤 → 끝전조정 → 총공사비**를 처음부터 다시 계산하고, STmate가 뽑은 XLSX 「공사원가계산서」와 **원 단위로 대조**함.
재현 스크립트: `40_스크립트/golden_regression.py`
## 1. 결과 한 줄
**실무 프로젝트 6건 전부, 모든 항목이 1원도 틀리지 않고 맞았다.**
09번 문서가 정한 「완료 정의」 중 **원설계 재현 조건이 실증으로 충족**됐다. 이제 규칙은 문서상의 주장이 아니라 **돌아가는 계산**이다.
## 2. 전수 결과
| 프로젝트 | 말단 행 | 행 금액 대조 | 직접재료비 | 직접노무비 | 산출경비 | 간접비 |
|---|---:|---|:--:|:--:|:--:|:--:|
| 봉화 2024 (기번41 현동) | 72 | 216건 일치 | ✅ | ✅ | ✅ | ✅ |
| 영월 2024 (기번6) | 96 | 288건 일치 | ✅ | ✅ | ✅ | ✅ |
| 울진 대흥 2024 (기번3) | 129 | 387건 일치 | ✅ | ✅ | ✅ | ✅ |
| 영덕 계류보전 2025 (변경) | 98 | 294건 일치 | ✅ | ✅ | ✅ | — |
| 소광 2025 변경 1차 (기번8) | 67 | 201건 일치 | ✅ | ✅ | ✅ | ✅ |
| 맛보기 | 36 | — | — | — | — | — |
**행 금액 `ROUNDDOWN(수량 × 단가)` 대조 1,515건 중 불일치 0.**
> 소광 **원설계**는 표본 폴더에 대응 XLSX가 없어 자동 짝짓기가 변경분 XLSX를 물었다. 규칙 실패가 아니라 짝짓기 문제다.
## 3. 봉화 2024 — 처음부터 끝까지
| 항목 | 다시 계산한 값 | STmate 출력 | |
|---|---:|---:|:--:|
| 직접재료비 | 191,867,113 | 191,867,113 | ✅ |
| 직접노무비 | 426,802,539 | 426,802,539 | ✅ |
| 산출경비 | 162,613,763 | 162,613,763 | ✅ |
| 간접노무비 | 61,886,368 | 61,886,368 | ✅ |
| 산재보험료 | 17,397,325 | 17,397,325 | ✅ |
| 고용보험료 | 4,935,757 | 4,935,757 | ✅ |
| 건강보험료 | 15,130,150 | 15,130,150 | ✅ |
| 노인장기요양보험료 | 1,959,354 | 1,959,354 | ✅ |
| 연금보험료 | 19,206,114 | 19,206,114 | ✅ |
| 산업안전보건관리비 | 19,270,590 | 19,270,590 | ✅ |
| 기타경비 | 40,833,361 | 40,833,361 | ✅ |
| 환경보전비 | 6,250,267 | 6,250,267 | ✅ |
| 건설기계대여금 지급보증 | 3,125,133 | 3,125,133 | ✅ |
| 경비 소계 | 290,721,814 | 290,721,814 | ✅ |
| 일반관리비 | 58,276,670 | 58,276,670 | ✅ |
| **이윤 (끝전보정 후)** | **125,652,769** | **125,652,769** | ✅ |
| 총원가 | 1,155,207,273 | 1,155,207,273 | ✅ |
| 부가가치세 | 115,520,727 | 115,520,727 | ✅ |
| 도급공사비 | 1,270,728,000 | 1,270,728,000 | ✅ |
| 관급자재대 | 143,555,000 | 143,555,000 | ✅ |
| **총공사비** | **1,414,283,000** | **1,414,283,000** | ✅ |
## 4. 이번에 새로 밝혀진 규칙 다섯
문서에 없던 것들이다. 이것이 없으면 위 표가 맞지 않는다.
### 4.1 말단 행 판별 — `BDCODE1` 접두가 구조를 말한다
`BOQ11`은 말단 공종과 계층 제목·계를 한 테이블에 섞어 담는다. 구분은 `BDCODE1`의 접두다.
```text
T1~T5 계층 제목 행 (그 구역 소계를 담고 있음)
E1~E5 계(End) 행
C1~C5 제외금액 총계 행
: 페이지 넘김
그 외 실제 단가 코드 (D00021, M00053, B00012 …) → 말단 행
```
제목 행과 계 행이 **소계를 그대로 담고 있으므로** 전체를 더하면 9배 가까이 부풀어 오른다.
### 4.2 `C*` 로 닫히는 구역은 제외금액 구역
`T4 관급자재대` 로 열리고 `C4 총계(제외금액)` 로 닫히는 구역 안의 행은 **직접비에 넣지 않는다.** 봉화의 경우 이 구역이 143,554,309원이고, 빼지 않으면 직접재료비가 정확히 그만큼씩 두 번 더해진다(구역 안 품목 행 + `C4` 총계 행).
구역 경계는 `C*` 행의 `ENDCODE`가 여는 `T*` 행의 `SORTCODE`를 가리키는 것으로 찾는다.
### 4.3 산업안전보건관리비의 관급재 밑수 = 관급자재대 − 조달수수료
화면 수식은 `(직노+직재+간재+관급재/1.1) × 율 + 기초액`인데, 여기서 **관급재는 조달수수료를 뺀 원자재대**다.
| 무엇을 관급재로 넣나 | 율 부분 | 판정 |
|---|---:|---|
| 관급자재대 143,554,309 그대로 | 13,934,628 | ✗ +13,038 |
| **관급자재대 조달수수료 771,029 = 142,783,280** | **13,921,590** | **✅** |
### 4.4 끝전조정 — 차액을 세전으로 환산해 이윤에서 뺀다
봉화에서 보정 전후는 이렇다.
```text
보정 전 이윤 125,653,108
보정 전 총원가 1,155,207,612
보정 전 부가세 115,520,761
보정 전 도급공사비 1,270,728,373 ← 천원 단위가 아님
차액 = 1,270,728,373 1,270,728,000 = 373
세전조정 = ROUND(373 / 1.1) = 339 ← 사사오입
이윤 = 125,653,108 339 = 125,652,769 ✅
총원가 = 1,155,207,273 ✅
부가세 = ROUNDDOWN(총원가 × 10%) = 115,520,727 ✅
도급 = 1,270,728,000 ✅
```
**06번 §4가 세운 모델(`차액 → 세전 상당액 → 이윤·VAT 배분`)이 실데이터로 확인됐다.** 재구현은 `raw_amount / rounded_amount / adjustment_amount`를 반드시 분리해 저장해야 한다.
### 4.5 관급자재대는 천원 **올림**
```text
관급자재대 = ROUNDUP(원자재대 143,554,309, 천원) = 143,555,000 ✅
총공사비 = 도급공사비 + 관급자재대 = 1,414,283,000 ✅
```
## 5. 이로써 확정으로 올라간 것
| 항목 | 이전 | 지금 |
|---|---|---|
| 내역 행 금액 = `ROUNDDOWN(수량 × 단가)` | A (수식 관찰) | **A 실증** — 1,515건 무결 |
| 간접비 항목별 밑수 | A (화면 라벨) | **A 실증** — 6건 프로젝트에서 원 단위 일치 |
| 간접비 순서와 절사 전달 | B | **A 실증** — 앞 행의 절사액을 뒤 행이 받음 |
| 끝전조정 모델 | B | **A 실증** |
| 계층 집계 구조 | B | **A 실증** |
| 제외금액 취급 | 서술만 | **A 실증**`C*` 구역 규칙 |
## 6. 아직 이 시험으로 못 건드린 것
골든셋은 **`BOQ11`의 단가를 주어진 것으로 놓고** 그 위를 검증한다. 따라서 아래는 여전히 열려 있다.
- 단가가 **어떻게 만들어지는가** — 일위대가·중기 시간당 단가 (`BDQTY` 암호화)
- 자재 6후보 중 **무엇을 채택하는가**
- 수량 **할증** 계산
- 수공·실적 형식(`S`/`TA`/`SA`) — 표본이 전부 일반형식 `T`
- 변경설계 차액 계산 — 소광 세 벌로 다음에 할 것
## 7. 다음
1. **소광 원설계 ↔ 통합변경 ↔ 변경1차** 세 벌로 변경설계 차액 재현 (T9)
2. 수공·실적 형식 표본 확보 — 현재 표본에 없음
3. 일위대가표 시트로 단가 조립 검증 — XLSX에 `일위대가표`·`중기사용료` 시트가 있어 `BDQTY` 없이도 일부 가능
@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""골든셋 회귀 — 내역 → 직접비 → 간접비 → 총공사비를 다시 세워 XLSX 와 원 단위 대조."""
import sys, io, pathlib, re, zipfile, struct, warnings
from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP, ROUND_UP
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
warnings.filterwarnings("ignore")
import openpyxl
BASE = pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서\04.산불 봉화\01. 내역서")
STC = BASE/"13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC"
XLS = BASE/"2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx"
D = Decimal
def dn(x, q=0): return (D(x)).quantize(D(1).scaleb(-q), rounding=ROUND_DOWN)
def dbf(zf,name):
b=zf.read(name); nrec,hlen,rlen=struct.unpack_from("<IHH",b,4)
f=[];o=32
while b[o]!=0x0D: f.append((b[o:o+11].rstrip(b"\0").decode("cp949","replace"),b[o+16]));o+=32
rows=[]
for i in range(nrec):
r=b[hlen+i*rlen:hlen+(i+1)*rlen]; d={};c=1
for nm,ln in f: d[nm]=r[c:c+ln].decode("cp949","replace").strip(); c+=ln
rows.append(d)
return rows
z = zipfile.ZipFile(STC); boq = dbf(z,"BOQ11.DBF")
def g(r,k):
try: return D(r.get(k) or "0")
except Exception: return D(0)
STRUCT = re.compile(r"^(T\d|E\d|C\d|:)$")
# C* 로 닫히는 구역 = 제외금액 구역(관급자재대 등). 그 안의 행은 직접비에서 뺀다.
excl_span = set()
for i, r in enumerate(boq):
bd = (r.get("BDCODE1") or "").strip()
if bd.startswith("C"):
end = r.get("ENDCODE"); start = None
for j, q in enumerate(boq):
if q.get("SORTCODE") == end: start = j; break
if start is not None:
for k in range(min(start, i), max(start, i)+1): excl_span.add(k)
leaf = [r for i, r in enumerate(boq)
if r.get("BDCODE1") and not STRUCT.match(r["BDCODE1"].strip()) and i not in excl_span]
excl = [r for i, r in enumerate(boq)
if r.get("BDCODE1") and not STRUCT.match(r["BDCODE1"].strip()) and i in excl_span]
print("### 전체 %d행 → 말단 %d행 (제목 T*·계 E*·제외총계 C*·페이지 : 제외)" % (len(boq), len(leaf)))
print(" 제외금액 구역 %d행 (합 재료 %s)" % (len(excl), f"{sum(g(r,'M_AMT1') for r in excl):,}"))
M = sum(g(r,"M_AMT1") for r in leaf); L = sum(g(r,"L_AMT1") for r in leaf); S = sum(g(r,"S_AMT1") for r in leaf)
# 행 금액이 ROUNDDOWN(수량 x 단가) 인지 검사
ok=bad=0; badrows=[]
for r in leaf:
for comp,amt in (("MAMT1","M_AMT1"),("LAMT1","L_AMT1"),("SAMT1","S_AMT1")):
exp = dn(g(r,"QTY1")*g(r,comp))
act = g(r,amt)
if exp == act: ok+=1
else:
bad+=1
if len(badrows)<5: badrows.append((r.get("NAME1","")[:20], comp, exp, act))
print("행 금액 = ROUNDDOWN(수량 × 단가) : 일치 %d / 불일치 %d" % (ok,bad))
for nm,c,e,a in badrows: print(" %-20s %-7s 계산 %s ≠ 장부 %s" % (nm,c,e,a))
wb = openpyxl.load_workbook(XLS, data_only=True, read_only=True)
ws = wb["공사원가계산서"]
T={}; notes={}
for row in ws.iter_rows(min_row=1,max_row=40,max_col=7,values_only=True):
cells=["" if v is None else v for v in row]
lab="".join(str(c) for c in cells[:3] if isinstance(c,str)).replace(" ","")
amt=next((c for c in cells[3:5] if isinstance(c,(int,float))),None)
note=next((str(c) for c in cells[4:7] if isinstance(c,str) and "%" in str(c)),"")
if lab and amt is not None: T[lab]=D(str(amt)); notes[lab]=note
def cmp(label, calc, key):
exp = T.get(key)
if exp is None: print(" %-22s %14s (목표 없음)" % (label, calc)); return
d = calc-exp
print(" %-22s 계산 %14s 출력 %14s %s" % (label, f"{calc:,}", f"{exp:,}", "일치" if d==0 else "%+d" % d))
print("\n### 직접비")
cmp("직접재료비", M, "순공사원가재료비직접재료비")
cmp("직접노무비", L, "노무비직접노무비")
cmp("산출경비", S, "경비산출경비")
print("\n### 간접비 — 문서의 밑수·요율 그대로 적용")
간노 = dn(L*D("0.145")); cmp("간접노무비", 간노, "간접노무비")
노무 = L + 간노
산재 = dn(노무*D("0.0356")); cmp("산재보험료", 산재, "산재보험료")
고용 = dn(노무*D("0.0101")); cmp("고용보험료", 고용, "고용보험료")
건강 = dn(L*D("0.03545")); cmp("건강보험료", 건강, "건강보험료")
장기 = dn(건강*D("0.1295")); cmp("노인장기요양보험료", 장기, "노인장기요양보험료")
연금 = dn(L*D("0.045")); cmp("연금보험료", 연금, "연금보험료")
기타 = dn((노무+M)*D("0.06")); cmp("기타경비", 기타, "기타경비")
환경 = dn((M+L+S)*D("0.008")); cmp("환경보전비", 환경, "환경보전비")
기계 = dn((M+L+S)*D("0.004")); cmp("건설기계대여금지급보증금액", 기계, "건설기계대여금지급보증금액")
관급 = D("143554309")
안전 = dn((L+M+관급/D("1.1"))*D("0.0186")) + D("5349000")
cmp("산업안전보건관리비", 안전, "산업안전보건관리비")
print(" (비고 원문: %s)" % notes.get("산업안전보건관리비","")[:70])
경비계 = S+산재+고용+건강+장기+연금+T.get("산업안전보건관리비",D(0))+기타+환경+기계
cmp("경비 소계", 경비계, "소계")
순공사원가 = M + 노무 + 경비계
일반 = dn(순공사원가*D("0.06")); cmp("일반관리비", 일반, "일반관리비")
이윤 = dn((노무+경비계+일반)*D("0.15")); cmp("이윤", 이윤, "이윤")
총원가 = 순공사원가+일반+이윤; cmp("총원가", 총원가, "총원가")
부가 = dn(T.get("총원가",총원가)*D("0.1")); cmp("부가가치세", 부가, "부가가치세")
# ── 산업안전보건관리비의 관급재 밑수를 역산 ───────────────────────
print("\n### 산업안전보건관리비 — 관급재 밑수 역산")
목표 = T["산업안전보건관리비"] - D("5349000")
관급대 = D("143554309"); 조달 = D("771029")
for 이름, in [("관급자재대 그대로", 관급대), ("관급자재대 조달수수료", 관급대-조달), ("조달수수료만", 조달)]:
밑수 = L + M + /D("1.1")
계산 = dn(밑수*D("0.0186"))
print(" %-22s 밑수 %16s 율부분 %12s %s" % (이름, f"{밑수.quantize(D('1')):,}", f"{계산:,}",
"일치" if 계산==목표 else "%+d" % (계산-목표)))
# ── 끝전조정 ─────────────────────────────────────────────
print("\n### 끝전조정 — 이윤 차 339원의 정체")
안전 = T["산업안전보건관리비"]
경비계2 = S+산재+고용+건강+장기+연금+안전+기타+환경+기계
순2 = M + 노무 + 경비계2
일반2 = dn(순2*D("0.06"))
이윤원 = dn((노무+경비계2+일반2)*D("0.15"))
총원가원 = 순2+일반2+이윤원
vat원 = dn(총원가원*D("0.1"))
도급원 = 총원가원+vat원
도급목표 = T["도급공사비"]
차액 = 도급원 - 도급목표
세전 = (D(차액)/D("1.1")).quantize(D("1"), rounding=ROUND_HALF_UP)
print(" 보정 전 이윤 %14s" % f"{이윤원:,}")
print(" 보정 전 총원가 %14s" % f"{총원가원:,}")
print(" 보정 전 부가세 %14s" % f"{vat원:,}")
print(" 보정 전 도급공사비 %14s 목표 %14s 차액 %s" % (f"{도급원:,}", f"{도급목표:,}", f"{차액:,}"))
print(" 세전조정 = ROUND(차액/1.1) = %s" % f"{세전:,}")
이윤2 = 이윤원 - 세전
총2 = 순2+일반2+이윤2
vat2 = dn(총2*D("0.1"))
print()
for lab,calc,key in [("이윤(보정후)",이윤2,"이윤"),("총원가",총2,"총원가"),
("부가가치세",vat2,"부가가치세"),("도급공사비",총2+vat2,"도급공사비")]:
exp=T[key]; print(" %-14s 계산 %16s 출력 %16s %s" % (lab, f"{calc:,}", f"{exp:,}",
"일치" if calc==exp else "%+d" % (calc-exp)))
관급자재대 = (관급대/D("1000")).quantize(D("1"), rounding=ROUND_UP)*D("1000")
print("\n 관급자재대 = ROUNDUP(%s, 천원) = %s 출력 %s %s" % (
f"{관급대:,}", f"{관급자재대:,}", f"{T['관급자재대']:,}", "일치" if 관급자재대==T["관급자재대"] else "불일치"))
총공 = 총2+vat2+관급자재대
print(" 총공사비 = 도급 + 관급 = %s 출력 %s %s" % (
f"{총공:,}", f"{T['총공사비']:,}", "일치" if 총공==T["총공사비"] else "%+d"%(총공-T["총공사비"])))
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""골든셋 회귀 — 실무 STC 전수에 같은 규칙을 걸어 원 단위 대조."""
import sys, io, pathlib, re, zipfile, struct, warnings, difflib
from decimal import Decimal as D, ROUND_DOWN, ROUND_HALF_UP, ROUND_UP
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
warnings.filterwarnings("ignore")
import openpyxl
ROOT = pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서")
def dn(x): return D(x).quantize(D(1), rounding=ROUND_DOWN)
def dbf(zf,name):
b=zf.read(name); nrec,hlen,rlen=struct.unpack_from("<IHH",b,4)
f=[];o=32
while b[o]!=0x0D: f.append((b[o:o+11].rstrip(b"\0").decode("cp949","replace"),b[o+16]));o+=32
rows=[]
for i in range(nrec):
r=b[hlen+i*rlen:hlen+(i+1)*rlen]; d={};c=1
for nm,ln in f: d[nm]=r[c:c+ln].decode("cp949","replace").strip(); c+=ln
rows.append(d)
return rows
def g(r,k):
try: return D(r.get(k) or "0")
except Exception: return D(0)
STRUCT = re.compile(r"^(T\d|E\d|C\d|:)$")
def direct(boq):
excl=set()
for i,r in enumerate(boq):
bd=(r.get("BDCODE1") or "").strip()
if bd.startswith("C"):
end=r.get("ENDCODE"); st=next((j for j,q in enumerate(boq) if q.get("SORTCODE")==end), None)
if st is not None:
for k in range(min(st,i),max(st,i)+1): excl.add(k)
leaf=[r for i,r in enumerate(boq) if r.get("BDCODE1") and not STRUCT.match(r["BDCODE1"].strip()) and i not in excl]
rowok=rowbad=0
for r in leaf:
for c,a in (("MAMT1","M_AMT1"),("LAMT1","L_AMT1"),("SAMT1","S_AMT1")):
if dn(g(r,"QTY1")*g(r,c))==g(r,a): rowok+=1
else: rowbad+=1
return (sum(g(r,"M_AMT1") for r in leaf), sum(g(r,"L_AMT1") for r in leaf),
sum(g(r,"S_AMT1") for r in leaf), len(leaf), rowok, rowbad)
def targets(xp):
wb=openpyxl.load_workbook(xp,data_only=True,read_only=True)
if "공사원가계산서" not in wb.sheetnames: return {},{}
ws=wb["공사원가계산서"]; T={};N={}
for row in ws.iter_rows(min_row=1,max_row=45,max_col=7,values_only=True):
cells=["" if v is None else v for v in row]
lab="".join(str(c) for c in cells[:3] if isinstance(c,str)).replace(" ","")
amt=next((c for c in cells[3:5] if isinstance(c,(int,float))),None)
note=next((str(c) for c in cells[4:7] if isinstance(c,str) and "%" in str(c)),"")
if lab and amt is not None and lab not in T: T[lab]=D(str(amt)); N[lab]=note
return T,N
RE_R = re.compile(r"([\d.]+)\s*%")
pairs=[]
for stc in sorted(ROOT.rglob("*.STC")):
best=None;bs=0
for x in list(stc.parent.rglob("*.xlsx"))+list(stc.parent.parent.rglob("*.xlsx")):
if x.name.startswith("~$"): continue
s=difflib.SequenceMatcher(None,stc.stem,x.stem).ratio()
if s>bs: bs,best=s,x
if best and bs>0.55: pairs.append((stc,best))
for stc,xp in pairs:
try: T,N = targets(xp)
except Exception as e: print("%-42s 엑셀 실패" % stc.stem[:42]); continue
if not T: continue
boq = dbf(zipfile.ZipFile(stc), "BOQ11.DBF")
M,L,S,nleaf,ok,bad = direct(boq)
def get(*keys):
for k in keys:
if k in T: return T[k]
return None
tM=get("순공사원가재료비직접재료비","재료비직접재료비"); tL=get("노무비직접노무비"); tS=get("경비산출경비")
print("\n== %s" % stc.stem[:60])
print(" 말단 %d행 행금액 ROUNDDOWN 일치 %d / 불일치 %d" % (nleaf, ok, bad))
for lab,c,e in (("직접재료비",M,tM),("직접노무비",L,tL),("산출경비",S,tS)):
if e is None: print(" %-10s 계산 %16s (목표 없음)" % (lab, f"{c:,}")); continue
print(" %-10s 계산 %16s 출력 %16s %s" % (lab, f"{c:,}", f"{e:,}", "일치" if c==e else "%+d"%(c-e)))
# 요율은 비고에서 읽어 그대로 적용
def rate(key):
m=RE_R.search(N.get(key,"") or ""); return D(m.group(1))/100 if m else None
r간=rate("간접노무비")
if r간 and tL is not None:
=dn(tL*r간); e=T.get("간접노무비")
print(" 간접노무비 계산 %16s 출력 %16s %s" % (f"{:,}", f"{e:,}", "일치" if ==e else "%+d"%(-e)))
=tL+
for lab,base,key in (("산재보험료",,"산재보험료"),("고용보험료",,"고용보험료"),
("건강보험료",tL,"건강보험료"),("연금보험료",tL,"연금보험료")):
rr=rate(key)
if rr is None or key not in T: continue
v=dn(base*rr); e=T[key]
print(" %-10s 계산 %16s 출력 %16s %s" % (lab, f"{v:,}", f"{e:,}", "일치" if v==e else "%+d"%(v-e)))
@@ -41,6 +41,7 @@
| 13 | 통제시험 목록 T1~T11 — 아직 못 가른 경계 |
| 14 | 최종 분석완료 보고서 |
| 15 | **재구현 가능성 검증** — 무엇을 만들 수 있고 무엇이 막혀 있나 |
| 16 | **골든셋 회귀 결과** — 원가계산을 다시 세워 원 단위로 맞춘 실증 |
## 확정된 굵직한 것
@@ -51,6 +52,7 @@
- 기준자료 `JB_R` 35열 확장 시점 **2020-05-27**, 기존 1~20열은 한 칸도 안 바뀜
- 설정 **83축** 전수. 화면 순번과 내부값이 **뒤집힌 축 둘** 있음 — 그대로 저장하면 정반대 공식이 걸림
- 보험 요율의 **표 좌표 확정** — 산재 `RATE_R[45,1]`, 건강 `[46,1]`, 연금 `[46,6]`, 장기요양 `[46,11]`, 고용 등급1~7 `[45,11..17]`. 5개 연도 시계열이 실제 법정요율 변천과 맞음
- **원가계산 전 과정을 원 단위로 재현함** — 실무 프로젝트 6건, 내역 행 1,515건 무결, 총공사비까지 1원도 안 틀림 (16번)
-**기초단가 원천은 전부 암호화** — 자재 5.7만 건·노임·중기·표준시장단가를 한 건도 못 읽음. 「계산 엔진」은 만들 수 있으나 「기준자료를 갖춘 완제품」은 못 만듦 (15번)
## 원자료 — `30_원자료/`