knowledge(경쟁사): 일위대가 참조 그래프·설계변경 직접비 실증 — 지금 자료로 할 것 마무리

일위대가 호표 참조 그래프 (④단계)
- 봉화 2024 의 45개 호표를 그래프로 세움
- 구성행이 다른 호표를 가리키고 단가가 일치하는 것 22건, 이름은 같은데
  단가가 다른 것 0건
- 순환 참조 없음 -> 위상순서 계산 가능. 깊이 최대 2단
- 09번의 「의존 그래프·위상순서·순환은 오류」가 실데이터에서 성립함

설계변경 직접비
- 계류보전 2025(변경) 의 BOQ11 은 한 표에 차수1(당초)·차수2(변경) 를 나란히 담음
- 양쪽을 각각 다시 세워 직접재료비·직접노무비·산출경비 전부 원 단위 일치
- 행금액 절사 대조 294건 x 2 = 588건 불일치 0
- 제외금액 구역 판정은 차수와 무관하게 같음
- 19번 6장의 간접비 모드와 합쳐 변경설계가 직접비부터 간접비까지 전부 재현됨

진행률 장부 갱신 — 지금 자료로 할 수 있는 일은 사실상 소진. 남은 것은
새 표본이 있어야 열림.

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 13:45:56 +09:00
co-authored by Claude Opus 5
parent 5e2a40b17a
commit b42793eb58
3 changed files with 183 additions and 3 deletions
@@ -13,7 +13,8 @@
| STC 안의 표 | 16종 | **10종 읽고 씀** · 4종 빈 표 | **2종 암호화** (`BDQTY`·`SYSINFO`) | ▓▓▓▓▓▓▓▓▓░ 88% |
| STmate 출력 `XLSX` | 6 | 6 전부 | — | ▓▓▓▓▓▓▓▓▓▓ 100% |
| 출력 시트 | **30종** | **30종 전부 훑음** (계산 있는 것 15종 규칙 확정) | — | ▓▓▓▓▓▓▓▓▓▓ 100% |
| 계산 사슬 단계 | 8 | **8 전부** | — | ▓▓▓▓▓▓▓▓▓▓ 100% |
| 계산 사슬 단계 | 8 | **8 전부** (④는 참조 그래프까지 실증) | — | ▓▓▓▓▓▓▓▓▓▓ 100% |
| 변경설계 재현 | 직접비+간접비 | **전 항목 원 단위 일치** | — | ▓▓▓▓▓▓▓▓▓▓ 100% |
| 09번이 꼽은 호환성 차단점 | 6 | **3 해결 · 1 부분** | 2 (실행 필요) | ▓▓▓▓▓▓░░░░ 58% |
## 2. 설치 폴더 186개
@@ -119,6 +120,36 @@
**재구현 필수** — 간접비 항목마다 `calc_mode ∈ {recalculated, carry_original, proportional_from_original, delta_times_rate, manual}` 을 저장할 것. 전부 재계산으로 두면 이 세 항목에서 **85만 원**이 어긋난다.
## 6.5 이번에 마저 끝낸 둘
### 일위대가 호표 참조 그래프 — ④단계 실증
봉화 2024의 `일위대가표` 45개 호표를 그래프로 세웠다.
| 항목 | 결과 |
|---|---|
| 구성행이 다른 호표를 가리키고 **단가가 일치** | **22건** |
| 이름은 같은데 단가가 다름 | **0건** |
| 호표 간 참조를 가진 호표 | 18 / 45 |
| **순환 참조** | **없음 → 위상순서 계산 가능** |
| 참조 깊이 | 최대 **2단** (잎 27 · 1단 14 · 2단 4) |
⇒ 09번 §3의 「의존 그래프를 만들고 위상순서로 계산, 순환은 오류」가 **실데이터에서 성립**한다. 깊이가 얕아 재귀 깊이 걱정은 없고, 이름+규격 짝으로 참조를 찾을 수 있다(불일치 0건).
### 설계변경 직접비 — 당초·변경 양쪽 원 단위 재현
계류보전 2025(변경)의 `BOQ11`은 한 표에 **차수1(당초)·차수2(변경)를 나란히** 담는다. 양쪽을 각각 다시 세웠다.
| | 직접재료비 | 직접노무비 | 산출경비 | 행금액 절사 |
|---|---:|---:|---:|---|
| 차수1 (당초) | 40,003,539 ✅ | 32,976,276 ✅ | 20,047,669 ✅ | 294건 무결 |
| 차수2 (변경) | 26,448,347 ✅ | 41,778,438 ✅ | 16,915,256 ✅ | 294건 무결 |
| **증감** | 13,555,192 | **+8,802,162** | 3,132,413 | |
**전 항목 1원도 안 틀림.** 같은 말단 행 집합(98행)에 차수별 수량·단가를 걸어 계산하면 되고, **제외금액 구역(`C*`) 판정도 차수와 무관하게 같다.**
이로써 6장의 간접비 모드와 합쳐 **변경설계가 직접비부터 간접비까지 전부 재현**된다.
## 7. 남은 일 — 할 수 있는 것과 없는 것
### 지금 자료로 할 수 있는 것
@@ -127,8 +158,9 @@
|---:|---|---|
| ~~1~~ | ~~남은 시트 훑기~~ | ✅ **완료** — 30종 전부. 새 규칙 없음, 집계표 ±1 원인 밝힘 |
| ~~2~~ | ~~중기시간금액집계표~~ | ✅ **완료** — 절사(수량 × 시간당 성분단가) |
| 1 | 일위대가 호표 **참조 그래프** 복원 → 위상순서 계산 재현 | ④단계를 실증으로 |
| 2 | 설계변경 **직접비 증감**까지 원 단위 재현 | 6장을 전 항목으로 넓힘 |
| ~~1~~ | ~~일위대가 참조 그래프~~ | ✅ **완료** — 순환 없음, 깊이 2단, 불일치 0 (§6.5) |
| ~~2~~ | ~~설계변경 직접비 증감~~ | ✅ **완료** — 당초·변경 전 항목 원 단위 일치 (§6.5) |
| 1 | **표본 늘리기** | 지금 자료로 할 수 있는 것은 사실상 다 함. 규모·기간 의존 요율 좌표와 수공·실적 형식은 **새 표본이 있어야** 열림 |
### 끝내 못 하는 것
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""설계변경 직접비 — 차수1(당초)·차수2(변경)를 내역에서 다시 세워 출력과 대조."""
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
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, s):
excl=set()
for i,r in enumerate(boq):
bd=(r.get("BDCODE"+s) 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"+s) or r.get("BDCODE1")) and not STRUCT.match((r.get("BDCODE"+s) or r.get("BDCODE1")).strip()) and i not in excl]
ok=bad=0
for r in leaf:
for c,a in (("MAMT","M_AMT"),("LAMT","L_AMT"),("SAMT","S_AMT")):
if dn(g(r,"QTY"+s)*g(r,c+s))==g(r,a+s): ok+=1
else: bad+=1
return (sum(g(r,"M_AMT"+s) for r in leaf), sum(g(r,"L_AMT"+s) for r in leaf),
sum(g(r,"S_AMT"+s) for r in leaf), len(leaf), ok, bad)
BASE=pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서\20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)")
stc=next(BASE.rglob("*.STC")); xls=next(p for p in BASE.rglob("*.xlsx") if p.stem==stc.stem)
boq=dbf(zipfile.ZipFile(stc),"BOQ11.DBF")
ws=openpyxl.load_workbook(xls,data_only=True)["공사원가계산서"]
T={}
for row in ws.iter_rows(min_row=1,max_row=45,max_col=6,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(" ","")
b=next((c for c in cells[3:4] if isinstance(c,(int,float))),None)
c2=next((c for c in cells[4:5] if isinstance(c,(int,float))),None)
if lab and b is not None: T[lab]=(D(str(b)), D(str(c2)) if c2 is not None else None)
print("### 설계변경 직접비 — 내역에서 다시 세움 (%s)" % stc.stem[:44])
for s,tag in (("1","당초"),("2","변경")):
M,L,S,n,ok,bad = direct(boq,s)
print("\n-- 차수%s (%s) 말단 %d행, 행금액 절사 일치 %d / 불일치 %d" % (s,tag,n,ok,bad))
for lab,calc,key in (("직접재료비",M,"순공사원가재료비직접재료비"),("직접노무비",L,"노무비직접노무비"),("산출경비",S,"경비산출경비")):
pair=T.get(key)
exp = pair[0] if s=="1" else (pair[1] if pair else None)
if exp is None: print(" %-10s 계산 %14s (목표 없음)"%(lab,f"{calc:,}")); continue
print(" %-10s 계산 %14s 출력 %14s %s"%(lab,f"{calc:,}",f"{exp:,}","일치" if calc==exp else "%+d"%(calc-exp)))
M1,L1,S1,*_=direct(boq,"1"); M2,L2,S2,*_=direct(boq,"2")
print("\n-- 증감")
for lab,a,b,key in (("직접재료비",M1,M2,"순공사원가재료비직접재료비"),("직접노무비",L1,L2,"노무비직접노무비"),("산출경비",S1,S2,"경비산출경비")):
print(" %-10s %+15s" % (lab, f"{b-a:,}"))
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""일위대가 호표 참조 그래프 — 구성행이 다른 호표를 가리키는지, 위상순서가 성립하는지."""
import sys, io, pathlib, warnings, re, collections
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
def dn(x): return D(x).quantize(D(1), rounding=ROUND_DOWN)
def dec(v):
if v in (None,""): return None
try: return D(str(v))
except Exception: return None
P = pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서\04.산불 봉화\01. 내역서\2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx")
wb = openpyxl.load_workbook(P, data_only=True)
def blocks(sheet):
ws=wb[sheet]
rows=[list(r) for r in ws.iter_rows(min_row=1,max_row=ws.max_row,max_col=12,values_only=True)]
out=[]; cur=None; title=None
for r in rows:
t=(str(r[0]) if r[0] else "").strip()
k=t.replace(" ","")
if re.match(r"^제?\d+호표$", k): cur={"no":int(re.sub(r"\D","",k)),"title":None,"comp":[],"sum":None}; out.append(cur); continue
if cur is None: continue
if cur["title"] is None and t and r[3] and not r[2]:
cur["title"]=(t, str(r[1] or "").strip(), str(r[3] or "")); continue
if k=="합계": cur["sum"]=r; cur=None; continue
if t: cur["comp"].append(r)
return out
B = blocks("일위대가표")
print("### 일위대가표 호표 %d" % len(B))
# 호표 합계(단가) 사전
unit = {}
for b in B:
if b["title"] and b["sum"]:
unit[(b["title"][0], b["title"][1])] = dec(b["sum"][5])
print(" 제목으로 찾을 수 있는 호표 %d" % len(unit))
edges=collections.Counter(); hit=miss=0; ex=[]
for b in B:
for c in b["comp"]:
nm=(str(c[0]) if c[0] else "").strip(); sz=str(c[1] or "").strip()
up=dec(c[4])
if up is None: continue
key=(nm,sz)
if key in unit and unit[key]==up:
edges[b["no"]]+=1; hit+=1
elif key in unit:
miss+=1
if len(ex)<4: ex.append((b["no"],nm[:16],sz[:12],up,unit[key]))
print("\n 구성행이 다른 호표를 가리키고 단가가 일치: %d" % hit)
print(" 이름은 같은데 단가가 다름: %d" % miss)
for e in ex: print("%d호표 %-16s %-12s 구성 %s ≠ 호표합계 %s" % e)
print("\n 호표 사이 참조를 가진 호표 %d개 (전체 %d개)" % (len(edges), len(B)))
# 순환 검사
adj=collections.defaultdict(set)
byname={}
for b in B:
if b["title"]: byname[(b["title"][0],b["title"][1])]=b["no"]
for b in B:
for c in b["comp"]:
key=((str(c[0]) if c[0] else "").strip(), str(c[1] or "").strip())
if key in byname and byname[key]!=b["no"]: adj[b["no"]].add(byname[key])
color={}
cyc=[]
def dfs(u, path):
color[u]=1
for v in adj[u]:
if color.get(v)==1: cyc.append(path+[u,v])
elif color.get(v) is None: dfs(v, path+[u])
color[u]=2
for b in B:
if color.get(b["no"]) is None: dfs(b["no"], [])
print(" 순환 참조: %s" % ("없음 → 위상순서 계산 가능" if not cyc else cyc[:2]))
depth={}
def dep(u):
if u in depth: return depth[u]
depth[u]=0
depth[u]= 1+max([dep(v) for v in adj[u]], default=-1)
return depth[u]
for b in B: dep(b["no"])
mx=max(depth.values()) if depth else 0
print(" 참조 깊이 최대 %d단 (분포: %s)" % (mx, dict(collections.Counter(depth.values()))))