knowledge(경쟁사): STmate 차수 규칙 실증 — BOQ12 가 현재안, 차수 인계 확인

골든셋 회귀에서 소광 한 건이 어긋난 원인을 파고든 결과.

- 한 STC 에 BOQ11·BOQ12 가 함께 있으면 출력은 BOQ12 를 씀.
  BOQ11 을 현재안으로 잡으면 그 프로젝트만 통째로 틀림
- 변경 파일의 BOQ11 차수1 이 앞 파일의 BOQ12 와 원 단위로 같음.
  차수 인계 메커니즘 실증
- PRJLST.TCOSTn = MCOSTn+LCOSTn+SCOSTn 이며 직접공사비 합계
- 소광 변경1차 직접공사비 173,311,386 -> 257,410,433, 증감 +84,099,047

회귀 스크립트가 현재안 표를 고르도록 고침. 이로써 실무 프로젝트 6건 전부
원 단위 일치, 내역 행 1,515건 불일치 0.

08번 문서의 차수 경고를 실제 규칙으로 승격. 문서 16 보강, 차수 추적
스크립트 추가.

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:21:14 +09:00
co-authored by Claude Opus 5
parent 87d4519ce0
commit 109e3846d0
4 changed files with 130 additions and 13 deletions
@@ -35,7 +35,22 @@ STC에는 두 형태가 동시에 확인된다.
1. 한 파일 안에 `BOQ11`, `BOQ12`를 함께 저장하는 통합형: 소광 표본은 각각 90행, 89행이다.
2. 변경 차수를 별도 STC로 저장하면서 그 파일의 `BOQ11`을 현재안으로 쓰는 분리형: 소광 변경1차 표본은 115행이다.
또한 `COSTN`과 여러 필드의 접미사 1~4는 원설계와 최대 3개 변경차수의 값을 나란히 담을 수 있다. 따라서 “접미사 n=항상 BOQ1n”으로 기계적으로 가정하지 말고 프로젝트 메타데이터와 실제 존재 테이블을 함께 해석해야 한다.
또한 `COSTN`과 여러 필드의 접미사 1~4는 원설계와 최대 3개 변경차수의 값을 나란히 담을 수 있다.
### 실증으로 확정된 차수 규칙 — A등급 (2026-09-13, 16번 §4.5·§4.6)
```text
통합형 한 STC 안에 BOQ11(당초) + BOQ12(현재안)
⚠ STmate 출력은 BOQ12 를 쓴다. BOQ11 을 현재안으로 잡으면 통째로 틀린다.
분리형 변경 차수를 새 STC 로 저장
새 파일 BOQ11 차수1 = 앞 파일의 BOQ12 ← 소광 표본에서 원 단위 일치 확인
새 파일 BOQ11 차수2 = 이번 변경분
```
`PRJLST.TCOSTn = MCOSTn + LCOSTn + SCOSTn` 으로 **직접공사비 합계**다. 소광 변경1차는 당초 173,311,386 → 변경 257,410,433, 증감 +84,099,047이다.
따라서 “접미사 n=항상 BOQ1n”으로 기계적으로 가정하지 말고 **실제 존재하는 테이블과 `PRJLST` 를 함께 보고 현재안을 고를 것.**
### STmate의 실제 변경설계 계산모드 — A등급
@@ -1,7 +1,7 @@
# 골든셋 회귀 결과 — 원가계산을 원 단위로 다시 세워 맞췄다
실행일: 2026-09-13
방법: 프로젝트 STC의 `BOQ11`만 입력으로 삼아 **직접비 → 간접비 → 이윤 → 끝전조정 → 총공사비**를 처음부터 다시 계산하고, STmate가 뽑은 XLSX 「공사원가계산서」와 **원 단위로 대조**함.
방법: 프로젝트 STC의 내역 표(`BOQ11`/`BOQ12`)만 입력으로 삼아 **직접비 → 간접비 → 이윤 → 끝전조정 → 총공사비**를 처음부터 다시 계산하고, STmate가 뽑은 XLSX 「공사원가계산서」와 **원 단위로 대조**함.
재현 스크립트: `40_스크립트/golden_regression.py`
## 1. 결과 한 줄
@@ -18,13 +18,11 @@
| 영월 2024 (기번6) | 96 | 288건 일치 | ✅ | ✅ | ✅ | ✅ |
| 울진 대흥 2024 (기번3) | 129 | 387건 일치 | ✅ | ✅ | ✅ | ✅ |
| 영덕 계류보전 2025 (변경) | 98 | 294건 일치 | ✅ | ✅ | ✅ | — |
| 소광 2025 원설계 (기번8) | 43 | 129건 일치 | ✅ | ✅ | ✅ | ✅ |
| 소광 2025 변경 1차 (기번8) | 67 | 201건 일치 | ✅ | ✅ | ✅ | ✅ |
| 맛보기 | 36 | — | — | — | — | — |
**행 금액 `ROUNDDOWN(수량 × 단가)` 대조 1,515건 중 불일치 0.**
> 소광 **원설계**는 표본 폴더에 대응 XLSX가 없어 자동 짝짓기가 변경분 XLSX를 물었다. 규칙 실패가 아니라 짝짓기 문제다.
## 3. 봉화 2024 — 처음부터 끝까지
| 항목 | 다시 계산한 값 | STmate 출력 | |
@@ -51,7 +49,7 @@
| 관급자재대 | 143,555,000 | 143,555,000 | ✅ |
| **총공사비** | **1,414,283,000** | **1,414,283,000** | ✅ |
## 4. 이번에 새로 밝혀진 규칙 다섯
## 4. 이번에 새로 밝혀진 규칙 일곱
문서에 없던 것들이다. 이것이 없으면 위 표가 맞지 않는다.
@@ -104,7 +102,37 @@ C1~C5 제외금액 총계 행
**06번 §4가 세운 모델(`차액 → 세전 상당액 → 이윤·VAT 배분`)이 실데이터로 확인됐다.** 재구현은 `raw_amount / rounded_amount / adjustment_amount`를 반드시 분리해 저장해야 한다.
### 4.5 관급자재대는 천원 **올림**
### 4.5 한 파일에 `BOQ11`·`BOQ12`가 같이 있으면 **`BOQ12`가 현재안**
소광 원설계 STC는 두 표를 함께 담는다.
| 표 | 행 | 말단 | 재료비 | 노무비 | 경비 | 출력과 |
|---|---:|---:|---:|---:|---:|:--:|
| `BOQ11` | 90 | 43 | 54,699,532 | 93,741,594 | 48,693,912 | ✗ |
| `BOQ12` | 89 | 43 | 47,987,107 | 82,246,518 | 43,077,761 | **✅** |
**STmate가 뽑은 XLSX는 `BOQ12`와 맞는다.** `BOQ11`을 무조건 현재안으로 잡으면 이 프로젝트만 통째로 틀린다. 처음 회귀에서 소광 한 건이 어긋난 원인이 이것이었다.
### 4.6 차수 인계 — 앞 파일의 `BOQ12`가 다음 파일의 `차수1`이 된다
| 무엇 | 재료비 | 노무비 | 경비 |
|---|---:|---:|---:|
| 원설계 파일 `BOQ12` | 47,987,107 | 82,246,518 | 43,077,761 |
| **변경1차 파일 `BOQ11` 차수1** | 47,987,107 | 82,246,518 | 43,077,761 |
| 변경1차 파일 `BOQ11` 차수2 | 75,735,506 | 119,733,078 | 61,941,849 |
**정확히 같다.** 08번이 「접미사 n = 항상 `BOQ1n` 으로 기계적으로 가정하지 말라」고 경고한 것이 옳았고, 실제 규칙은 이렇다.
```text
통합형 : 한 STC 안에 BOQ11(당초) + BOQ12(현재안). 출력은 BOQ12.
분리형 : 변경 차수를 새 STC 로 저장.
그 파일의 BOQ11 차수1 = 앞 파일의 BOQ12
그 파일의 BOQ11 차수2 = 이번 변경분
```
`PRJLST.TCOSTn = MCOSTn + LCOSTn + SCOSTn` 이며 **직접공사비 합계**다. 소광 변경1차는 당초 173,311,386 → 변경 257,410,433, 증감 **+84,099,047**이다.
### 4.7 관급자재대는 천원 **올림**
```text
관급자재대 = ROUNDUP(원자재대 143,554,309, 천원) = 143,555,000 ✅
@@ -124,16 +152,16 @@ C1~C5 제외금액 총계 행
## 6. 아직 이 시험으로 못 건드린 것
골든셋은 **`BOQ11`의 단가를 주어진 것으로 놓고** 그 위를 검증한다. 따라서 아래는 여전히 열려 있다.
골든셋은 **내역 표의 단가를 주어진 것으로 놓고** 그 위를 검증한다. 따라서 아래는 여전히 열려 있다.
- 단가가 **어떻게 만들어지는가** — 일위대가·중기 시간당 단가 (`BDQTY` 암호화)
- 자재 6후보 중 **무엇을 채택하는가**
- 수량 **할증** 계산
- 수공·실적 형식(`S`/`TA`/`SA`) — 표본이 전부 일반형식 `T`
- 변경설계 차액 계산 — 소광 세 벌로 다음에 할 것
- 변경설계 **간접비**의 계산모드차수 인계와 직접비 증감(+84,099,047)까지는 확인했으나, 변경분 XLSX 출력이 표본에 없어 간접비 쪽 모드(방법 A/B/직접입력)는 아직 못 가름
## 7. 다음
1. **소광 원설계 ↔ 통합변경 ↔ 변경1차** 세 벌로 변경설계 차액 재현 (T9)
2. 수공·실적 형식 표본 확보 — 현재 표본에 없음
3. 일위대가표 시트로 단가 조립 검증 — XLSX에 `일위대가표`·`중기사용료` 시트가 있어 `BDQTY` 없이도 일부 가능
1. **일위대가표·중기사용료 시트로 단가 조립 검증** — XLSX에 두 시트가 있어 `BDQTY` 암호화와 무관하게 단가가 어떻게 쌓이는지 확인 가능. 지금 자료로 되는 다음 칸
2. 변경설계 **간접비** 모드 — 변경분 출력 XLSX 한 건만 있으면 정적으로 갈림
3. 수공·실적 형식 표본 확보 — 현재 표본이 전부 일반형식 `T`
@@ -66,7 +66,11 @@ 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")
zf = zipfile.ZipFile(stc)
have = {n.upper() for n in zf.namelist()}
# 한 파일에 BOQ11·BOQ12 가 함께 있으면 BOQ12 가 현재안(출력 기준)이다
tbl = "BOQ12.DBF" if "BOQ12.DBF" in have and dbf(zf,"BOQ12.DBF") else "BOQ11.DBF"
boq = dbf(zf, tbl)
M,L,S,nleaf,ok,bad = direct(boq)
def get(*keys):
for k in keys:
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
import sys, io, pathlib, zipfile, struct, re, warnings
from decimal import Decimal as D, ROUND_DOWN
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
warnings.filterwarnings("ignore"); import openpyxl
B = pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서\25년 산불진화임도(기번8)(울진소광)")
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)
def dn(x): return D(x).quantize(D(1), rounding=ROUND_DOWN)
STRUCT = re.compile(r"^(T\d|E\d|C\d|:)$")
def direct(boq, sfx="1"):
excl=set()
for i,r in enumerate(boq):
bd=(r.get("BDCODE"+sfx) or 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("BDCODE"+sfx) or r.get("BDCODE1")) and not STRUCT.match((r.get("BDCODE"+sfx) or r.get("BDCODE1")).strip()) and i not in excl]
return (sum(g(r,"M_AMT"+sfx) for r in leaf), sum(g(r,"L_AMT"+sfx) for r in leaf),
sum(g(r,"S_AMT"+sfx) for r in leaf), len(leaf))
orig = zipfile.ZipFile(B/"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC")
chg = zipfile.ZipFile(B/"변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC")
b11 = dbf(orig,"BOQ11.DBF"); b12 = dbf(orig,"BOQ12.DBF"); c11 = dbf(chg,"BOQ11.DBF")
print("### 원설계 파일 안의 두 표")
for nm,t in (("BOQ11",b11),("BOQ12",b12)):
M,L,S,n = direct(t)
print(" %-6s %3d행 → 말단 %3d 재료 %13s 노무 %13s 경비 %13s" % (nm,len(t),n,f"{M:,}",f"{L:,}",f"{S:,}"))
wb = openpyxl.load_workbook(B/"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", data_only=True, read_only=True)
ws = wb["공사원가계산서"]; T={}
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)
if lab and amt is not None and lab not in T: T[lab]=D(str(amt))
print("\n### 출력 XLSX 가 어느 표를 썼나")
tM=T.get("순공사원가재료비직접재료비"); tL=T.get("노무비직접노무비"); tS=T.get("경비산출경비")
print(" 출력 재료 %13s 노무 %13s 경비 %13s" % (f"{tM:,}",f"{tL:,}",f"{tS:,}"))
for nm,t in (("BOQ11",b11),("BOQ12",b12)):
M,L,S,_ = direct(t)
print(" %-6s %s" % (nm, "✅ 일치" if (M,L,S)==(tM,tL,tS) else "불일치"))
print("\n### 변경1차 파일의 BOQ11 차수1 이 원설계 파일의 BOQ12 와 같은가")
M1,L1,S1,n1 = direct(c11,"1"); M2,L2,S2,n2 = direct(c11,"2")
M12,L12,S12,_ = direct(b12)
print(" 변경파일 차수1 말단 %3d 재료 %13s 노무 %13s 경비 %13s" % (n1,f"{M1:,}",f"{L1:,}",f"{S1:,}"))
print(" 원설계 BOQ12 재료 %13s 노무 %13s 경비 %13s %s" % (f"{M12:,}",f"{L12:,}",f"{S12:,}",
"✅ 같음" if (M1,L1,S1)==(M12,L12,S12) else "다름"))
print(" 변경파일 차수2 말단 %3d 재료 %13s 노무 %13s 경비 %13s" % (n2,f"{M2:,}",f"{L2:,}",f"{S2:,}"))
print("\n 직접공사비 당초 %s → 변경 %s 증감 %s" % (
f"{M1+L1+S1:,}", f"{M2+L2+S2:,}", f"{(M2+L2+S2)-(M1+L1+S1):+,}"))
pl = dbf(chg,"PRJLST.DBF")[0]
print(" PRJLST 1차 T=%s 2차 T=%s (직접비 합과 %s)" % (
f"{g(pl,'TCOST1'):,}", f"{g(pl,'TCOST2'):,}",
"일치" if (g(pl,'TCOST1')==M1+L1+S1 and g(pl,'TCOST2')==M2+L2+S2) else "불일치"))