Files
Pier_v1/검토파일/검토_운곡교P1/스크립트/make_bom_compare.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

693 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""운곡교 P1 철근재료표 대조 (도면 재료표와 같은 양식으로 도면표 재현 → 모델표 작성 → 대조·비고).
사용: python -I make_bom_compare.py <pylib경로> <출력xlsx>
입력(고정): 바이브코딩 폴더의 3.dxf (도면 재료표), 운곡교p1.3dm (모델)
시트: ①도면 재료표(재현) ②모델 재료표 ③대조·비고 ④요약 ⑤비고코드설명 ⑥모델_개별철근
"""
import sys, re, math, collections
sys.path.insert(0, sys.argv[1])
import ezdxf, rhino3dm
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
BASE = r"D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩"
DXF3 = BASE + r"\3.dxf"
M3DM = BASE + r"\운곡교p1.3dm"
OUT = sys.argv[2]
UNIT = {"32": 6.230, "29": 5.040, "25": 3.980, "22": 3.040, "19": 2.250, "16": 1.560}
TOL = 1.0 + 1e-6 # mm : 도면 값은 mm 로 반올림 표기
# =============================================================== 1. 도면 재료표 파싱
COLS = [("num", 1900, 2500), ("shape", 2500, 3500), ("dia", 3500, 4400), ("n", 4400, 5300),
("A", 5300, 6700), ("B", 6700, 8200), ("C", 8200, 9700), ("D", 9700, 11200),
("E", 11200, 12700), ("R", 12700, 14200), ("X1", 14200, 15600), ("X2", 15600, 17100),
("J", 17100, 18800), ("L", 18800, 20500), ("tot", 20500, 21400), ("unit", 21400, 22500),
("w", 22500, 23600), ("wh", 23600, 25000)]
OFF = 23750
doc = ezdxf.readfile(DXF3)
texts = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
texts.append((e.dxf.insert.x, e.dxf.insert.y, e.dxf.text))
elif e.dxftype() == "MTEXT":
texts.append((e.dxf.insert.x, e.dxf.insert.y, e.plain_text()))
rowsraw = collections.defaultdict(lambda: collections.defaultdict(list))
for x, y, t in texts:
if not (18800 < y < 30000):
continue
tab = "SD500" if x >= 25000 else "SD400"
xx = x - OFF if tab == "SD500" else x
if 8500 <= xx < 12500 and "계" in t:
rowsraw[(tab, round(y / 10) * 10)]["label"].append(t.strip().replace("%%%", "%"))
continue
for name, lo, hi in COLS:
if lo <= xx < hi:
rowsraw[(tab, round(y / 10) * 10)][name].append((xx, t.strip().replace("%%%", "%")))
break
def first(c, k):
v = c.get(k)
if not v:
return ""
return sorted(v)[0][1] if isinstance(v[0], tuple) else v[0]
drow = {"SD400": [], "SD500": []} # 도면 행 (위→아래)
for (tab, y), c in sorted(rowsraw.items(), key=lambda kv: (kv[0][0], -kv[0][1])):
lab = " ".join(c.get("label", []))
num = first(c, "num")
if lab:
if y > 25000 + 100 and tab == "SD500" and "겹이음" in lab:
pass
drow[tab].append({"kind": "sum", "label": lab, "cells": {k: first(c, k) for k in ("tot", "unit", "w", "wh")}})
elif re.match(r"^[A-Z]{1,2}\d+$", num):
d = {k: first(c, k) for k, _, _ in COLS}
drow[tab].append({"kind": "bar", "mark": num, "cells": d})
# 겹이음 총계 행(라벨 위치가 달라 별도 수집): 도면 값만
for (tab, y), c in rowsraw.items():
pass
def mm(s):
"""도면 표기 → mm 리스트 (범위는 큰 값~작은 값)"""
if s is None or s == "" or s == '"':
return None
s = s.replace("°", "")
out = []
for part in s.split("~"):
try:
v = float(part)
except ValueError:
return None
out.append(v * 1000.0 if ("." in part and abs(v) < 100) else v)
return out
# 도면 행의 ditto(") 해소
for tab in drow:
last = {}
for r in drow[tab]:
if r["kind"] != "bar":
continue
for k in ("shape", "dia"):
if r["cells"][k] == '"':
r["cells"][k + "_v"] = last[k]
else:
r["cells"][k + "_v"] = r["cells"][k]
last[k] = r["cells"][k]
r["dia_n"] = re.sub(r"\D", "", r["cells"]["dia_v"])
DR = {r["mark"]: r for tab in drow for r in drow[tab] if r["kind"] == "bar"}
# =============================================================== 2. 모델 읽기 / 치수 추출
m3 = rhino3dm.File3dm.Read(M3DM)
lay = {l.Index: l.FullPath for l in m3.Layers}
def V(p): return (p.X, p.Y, p.Z)
def sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def dot(a, b): return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def nrm(a): return math.sqrt(dot(a, a))
def addv(a, b, t): return (a[0] + b[0] * t, a[1] + b[1] * t, a[2] + b[2] * t)
def isect(p1, d1, p2, d2):
w = sub(p1, p2); a = dot(d1, d1); b = dot(d1, d2); c = dot(d2, d2); d = dot(d1, w); e = dot(d2, w)
den = a * c - b * b
if abs(den) < 1e-9:
return None
return addv(p1, d1, (b * e - c * d) / den)
def segs_of(g):
if isinstance(g, rhino3dm.PolyCurve):
return [g.SegmentCurve(i) for i in range(g.SegmentCount)]
return [g]
def is_line(s): return type(s).__name__ in ("LineCurve", "PolylineCurve")
def clen(c, n=3000):
d = c.Domain; p0 = c.PointAt(d.T0); s = 0.0
for i in range(1, n + 1):
p = c.PointAt(d.T0 + (d.T1 - d.T0) * i / n)
s += nrm(sub(V(p), V(p0))); p0 = p
return s
def arc_len(s): return s.Arc.Radius * math.radians(s.Arc.AngleDegrees)
def corner_seq(g):
"""직선 구간을 연장해 만든 꼭짓점 사이 거리(각진 치수). 닫힌 곡선은 시작/끝 변을 합침."""
lines = [(V(s.PointAtStart), V(s.PointAtEnd)) for s in segs_of(g) if is_line(s)]
closed = nrm(sub(V(g.PointAtStart), V(g.PointAtEnd))) < 1e-3
pts = []
for i in range(len(lines) - 1):
p1, q1 = lines[i]; p2, q2 = lines[i + 1]
pts.append(isect(p1, sub(q1, p1), p2, sub(q2, p2)))
if closed:
out = [nrm(sub(pts[i + 1], pts[i])) for i in range(len(pts) - 1)]
out.append(nrm(sub(pts[0], lines[0][0])) + nrm(sub(lines[-1][1], pts[-1])))
return out, lines, pts
seq = [lines[0][0]] + pts + [lines[-1][1]]
return [nrm(sub(seq[i + 1], seq[i])) for i in range(len(seq) - 1)], lines, pts
def ang_at(corner, a, b):
va = sub(a, corner); vb = sub(b, corner)
c = dot(va, vb) / (nrm(va) * nrm(vb))
return math.degrees(math.acos(max(-1, min(1, c))))
def num(s):
try: return float(s)
except Exception: return 0.0
def bar_dims(shape, us, g):
"""한 철근의 도면 재료표 항목(A,B,C,D,E,R,X1,X2) 을 mm / 도(°) 로 반환"""
d = {}
if shape in ("I", "XVI"):
d["A"] = corner_seq(g)[0][0]
elif shape in ("III", "XII"):
seq = corner_seq(g)[0]
d["A"] = seq[1]; d["B"] = max(seq[0], seq[2]) if abs(seq[0] - seq[2]) > 1 else seq[0]
if abs(seq[0] - seq[2]) > 1: d["B2"] = min(seq[0], seq[2])
elif shape == "II":
seq = corner_seq(g)[0]
d["A"] = max(seq); d["B"] = min(seq)
elif shape == "VIII":
seq, lines, pts = corner_seq(g)
d["A"] = seq[1]; d["B"] = seq[0]; d["C"] = seq[2]
d["X1"] = ang_at(pts[0], lines[0][0], pts[1]); d["X2"] = ang_at(pts[1], lines[2][1], pts[0])
elif shape == "VII":
ls = [s for s in segs_of(g) if is_line(s)]
arcs = [s for s in segs_of(g) if type(s).__name__ == "ArcCurve"]
d["A"] = nrm(sub(V(ls[0].PointAtEnd), V(ls[0].PointAtStart)))
d["C"] = nrm(sub(V(ls[1].PointAtEnd), V(ls[1].PointAtStart)))
d["E"] = nrm(sub(V(ls[2].PointAtEnd), V(ls[2].PointAtStart)))
d["R"] = arcs[0].Arc.Radius
elif shape == "IV":
d["A"] = num(us.get("w_model")); d["B"] = num(us.get("b_sched"))
mm_ = re.search(r"laps?\s*\d+\s*x\s*(\d+)", us.get("src", ""))
d["C"] = float(mm_.group(1)) if mm_ else None
elif shape == "X":
d["A"] = num(us.get("a_sched")); d["B"] = num(us.get("w_model"))
mm_ = re.search(r"laps?\s*\d+\s*x\s*(\d+)", us.get("src", ""))
d["C"] = float(mm_.group(1)) if mm_ else None
elif shape == "XIII":
seq = corner_seq(g)[0]
d["A"] = seq[0]; d["B"] = seq[1]
elif shape == "XIV":
ls = [s for s in segs_of(g) if is_line(s)]
arcs = [s for s in segs_of(g) if type(s).__name__ == "ArcCurve"]
d["A"] = max(nrm(sub(V(s.PointAtEnd), V(s.PointAtStart))) for s in ls)
d["C"] = min(nrm(sub(V(s.PointAtEnd), V(s.PointAtStart))) for s in ls)
d["B"] = arc_len(arcs[0]); d["R"] = arcs[0].Arc.Radius
elif shape in ("V", "IX"):
s = segs_of(g)[0]
d["A"] = arc_len(s); d["B"] = num(us.get("coupler_length")); d["R"] = s.Arc.Radius
elif shape == "VI":
segs = segs_of(g)
main = max((s for s in segs if type(s).__name__ == "ArcCurve"), key=lambda s: s.Arc.Radius)
ls = [s for s in segs if is_line(s)]
d["R"] = main.Arc.Radius
# A·B 는 갈고리(약 142° 굽힘) 각진 치수 정의가 도면에 없어 기하로 확정할 수 없음 → 미추출(None)
# (접선 연장 방식으로 재면 A 2.837 / B 212 로 도면 2.818 / 200 과 다르고 합 A+2B 도 모델 L 과 안 맞음)
elif shape == "XI":
seq = corner_seq(g)[0]
d["A"] = seq[1]; d["B"] = seq[0]
d["C"] = num(us.get("design_length")) - seq[0] - seq[1] # 상단 180° 연장(미작도, 길이만 반영)
return d
bars = collections.defaultdict(dict)
for o in m3.Objects:
L = lay.get(o.Attributes.LayerIndex, "")
if not L.startswith("P1::철근"):
continue
g = o.Geometry; nm = o.Attributes.Name or ""
if isinstance(g, rhino3dm.Brep):
us = dict(o.Attributes.GetUserStrings())
bars[us.get("mark")].setdefault(us.get("occurrence"), {}).update(us=us, name=nm, layer=L)
elif L.endswith("중심선"):
p = nm.split("-")
if len(p) >= 4 and p[1] == "RC":
b = bars[p[2]].setdefault("-".join(p[3:]), {})
b["g"] = g
mod = {}
for mk, dd in bars.items():
if mk is None:
continue
items = [b for b in dd.values() if "us" in b and "g" in b]
if not items:
continue
shape = items[0]["us"].get("shape")
Ls = []; dims = collections.defaultdict(list); cl = 0.0
for b in items:
us = b["us"]
Ls.append(num(us.get("design_length")) + num(us.get("coupler_length")))
cl += clen(b["g"])
for k, v in bar_dims(shape, us, b["g"]).items():
if v is not None:
dims[k].append(v)
half = mk in ("P3", "P4")
n = len(items) // 2 if half else len(items)
mod[mk] = dict(shape=shape, dia=items[0]["us"].get("dia"), n=n, pieces=len(items), tot=sum(Ls) / 1000.0,
L=sum(Ls) / n, Lmin=min(Ls), Lmax=max(Ls), dims=dims, cl=cl / 1000.0, items=items, half=half)
# =============================================================== 3. 표기·비교 도구
def fmt_one(v, key=None):
if key in ("X1", "X2"):
return "%d°" % round(v)
return "%.3f" % (v / 1000.0) if v >= 1000 else "%d" % round(v)
def agg(vals, key):
"""철근별 값 목록 → (표기문자열, 대표 리스트[큰→작은])"""
if not vals:
return "", None
if key in ("X1", "X2"):
vals = [round(v) for v in vals]
lo, hi = min(vals), max(vals)
if hi - lo <= 0.6:
return fmt_one(vals[0], key), [vals[0]]
if key == "A" or key == "B" or key == "C":
# 도면은 '큰값~작은값'
return fmt_one(hi, key) + "~" + fmt_one(lo, key), [hi, lo]
return fmt_one(hi, key) + "~" + fmt_one(lo, key), [hi, lo]
def same(dtxt, mvals, key):
"""도면 표기(dtxt) 와 모델 값 일치 여부. 반환: 'same' | 'diff' | 'drawing_blank' | 'model_blank' | 'both_blank'"""
dv = mm(dtxt) if dtxt else None
if dv is None and mvals is None:
return "both_blank"
if dv is None:
return "drawing_blank"
if mvals is None:
return "model_blank"
if key in ("X1", "X2"):
return "same" if [round(x) for x in dv] == [round(x) for x in mvals] else "diff"
if len(dv) != len(mvals):
return "diff"
return "same" if all(abs(a - b) <= TOL for a, b in zip(dv, mvals)) else "diff"
DIMK = ["A", "B", "C", "D", "E", "R", "X1", "X2", "J"]
def model_cells(mk):
"""도면 표와 같은 칸 구성으로 모델 값 문자열/리스트 생성"""
m = mod.get(mk)
cells = {}; vals = {}
if not m:
return cells, vals
for k in DIMK:
t, v = agg(m["dims"].get(k, []), k)
cells[k] = t; vals[k] = v
return cells, vals
# =============================================================== 4. 비고 (수동 코드 + 자동 차이)
NOTE = {}
KDS = {}
for mk in ("C1", "C4"):
NOTE[mk] = "[D-8] 도면 내부: 재료표 다리각 98°/96° ↔ 정면도 96.4°."
for mk in ("C2", "C5"):
NOTE[mk] = "[D-7] 도면 내부: 재료표 L 12.081 ↔ 치수 합 A+B+C 12.082 (모델은 합계 12.082). [D-8] 다리각 98°/96° ↔ 정면도 96.4°."
for mk in ("C3", "C6"):
NOTE[mk] = "[D-8] 도면 내부: 재료표 다리각 98°/96° ↔ 정면도 96.4°."
for mk in ("C7", "C8"):
NOTE[mk] = "[D-9] 도면 내부: 재료표 B·D·R·X·J 빈칸 → 모델은 정면도 R 12,100(하면 R 12,000 동심)·평면부 EL 40.511 로 작성. [D-10] 다리 650 정면도 겹침으로 확인 불가."
NOTE["C9"] = "[D-4] 스터럽 폭 도면 재료표 1.407 ↔ 단면 작도 1.160/1.740/1.160 (모델은 작도값). [D-6] 재료표 L=(최대+최소)/2 근사 → 모델은 철근마다 개별 변길이. [M-5] 길이 산식 +54.3 규칙은 변하는 치수에만 적용."
NOTE["C10"] = "[D-4] 스터럽 폭 1.407 ↔ 작도 1.160/1.740 (모델은 작도값). [M-5] +54.3 규칙."
NOTE["C11"] = "[D-4] 스터럽 폭 1.407 ↔ 작도 1.160/1.740/1.160. [D-6] (최대+최소)/2 근사 vs 모델 개별 변길이. [M-5] +54.3 규칙."
NOTE["C12"] = "[D-5] 수평폐합근 폭 1.407 ↔ 작도 1.106/1.686/1.106 (모델은 작도값). [D-6] 길이 (최대+최소)/2 근사 vs 모델 개별. [M-5] +54.3 규칙."
NOTE["C13"] = "[D-5] 수평폐합근 폭 1.407 ↔ 작도 1.106/1.686/1.106. [D-6] (최대+최소)/2 근사 vs 모델 개별. [M-5] +54.3 규칙."
NOTE["C14"] = "[M-1] 모델 최하단 점 2개(x=±1913, ±2913)가 도면보다 350 mm 높음(근거 기록 없음). [M-2] 훅 방향 도면 표기 없음 — 모델은 아래로 가정, 도면 점 위치로는 하부 점은 위·상부 점은 아래일 가능성(추정, 간접 확인)."
NOTE["C16"] = "훅 방향·정면도 좌측 끝은 도면 직접 표기 없음 (재료표 L 5.611 = 5.261 + 0.350 한쪽 훅으로 간접 확인)."
for mk in ("S1", "S2"):
NOTE[mk] = "[M-6] 높이 기준: 도면(D2 전열 상세)은 대좌 상면 -50 ↔ 모델은 코핑 상면 -50 (139~164 mm 낮음). [M-7] 층 순서: 도면은 S2 점이 S1 선 위 ↔ 모델은 S1 위·S2 50 mm 아래(모델 src '추론')."
NOTE["UA1"] = "[M-13] 모델에 없음(0본). 도면은 기초 조립용 '배치간격 예시도'(1.0×1.2 예시)라 수량 73본 근거 미확인."
NOTE["P1"] = "[D-20] 주철근 상단: 정면도 EL 43.011 ↔ 재료표 EL 43.061 (모델은 재료표). [D-21] 반지름 치수·선 875 ↔ 점 870.5 (모델은 점)."
NOTE["P2"] = "[D-20] 주철근 상단: 정면도 EL 40.411 ↔ 재료표 EL 43.061 (코핑 정착 2.65 m 차이, 모델은 재료표). [D-21] 반지름 830 ↔ 점 825.5 (모델은 점)."
NOTE["P3"] = "모델은 반원 2조각(72개)=1링 → 개수는 링 수(36)로 환산. A·B 는 모델 갈고리(굽힘 142°·R42.75)의 각진 치수 정의가 도면에 없어 기하로 추출하지 못함(미추출) — 길이 L 6.436·R 897·개수는 일치. [M-12] 단마다 90° 엇갈린 이음 각도는 도면에 기준 없음(모델 가정, 간접)."
NOTE["P4"] = "모델은 반원 2조각(18개)=1링(커플러 65 포함 L 5.636) → 개수 9 로 환산. [D-23] 라벨 H19 @100 ↔ 치수 8@120=960 (모델 @120 — 구조계산서 확인 사항, E-01). [M-12] 커플러 각도 도면에 없음."
NOTE["P5"] = "[D-23] 라벨 @100 ↔ 치수 22@120=2,640 (모델 @120, 구조계산서 확인). [D-22] 링 점 902 ↔ R 897. [M-12] 커플러 각도 도면에 없음."
NOTE["P6"] = "정면도 선 반길이 875 ↔ 모델 직선 894 (한쪽 19 mm, 경미)."
NOTE["B1"] = "[M-8] 모델 교차 철근 간격 54~56 mm ↔ 도면 두 뷰 모두 27~28 mm(점 값을 서로 다른 뷰에서 가져옴: B1 2073 ↔ B5 2127). [D-15] 같은 철근을 점/선으로 그린 높이가 정면도·측면도에서 27~28 mm 다름."
NOTE["B2"] = "[M-9] 다리 끝 +2072 (도면 선 기준 +2100). [D-15] 점/선 높이 27~28 mm 차."
NOTE["B3"] = "[D-17] 도면 중앙 치수 33@250+450(36본) ↔ 점·재료표 38본 (모델 38)."
NOTE["B4"] = "[M-8] 하면 교차 철근 간격 영향(B2·B4)."
NOTE["B5"] = "[M-9] 모델 중심 73 아래(순피복 58.5 mm, 도면 표기 상면 100, 선행 E-06). [M-8] 교차 철근 간격."
NOTE["B6"] = "[M-9] 다리 끝 +2128 (도면 선 기준 +2100, 선행 E-06 피복 57.5). [M-8] 교차 철근 간격."
NOTE["B7"] = "[M-8] 교차 철근 간격."
NOTE["B9"] = "[M-01 선행] 도면 개수 빈칸. 라벨 H16 @500 + 격자 34×29(@250) 체크무늬 986/2 = 493본을 총길이 1,062.415 ÷ L 2.155 로 역산(간접 확인) → 모델 493본. 상단 180° 연장 C=64 는 모델에 직선으로 작도되지 않고 길이만 반영."
NOTE["B10"] = "[D-18] 치수 9@200(10단) ↔ 점·재료표 9단 (모델 9단), 표 9.198×7.644 ↔ 점 ±4593/±3818."
NOTE["B11"] = "B11·B12 4단 높이(z 550)와 위치 일치."
NOTE["B12"] = "[D-16] 수량 34본(재료표) ↔ 모델 28본. 도면이 간소화되어 작도에 일부만 남아 있어 모델 작성 시 도면을 정확히 인식하지 못함. 정면도 간격 1000 한 줄, 단면도 400 위치에서 엇갈려 1000 간격으로 배치되는 철근이라 재료표 34본이 맞는 것으로 보임 → 설계자의 판단(확인)이 필요한 부분."
for mk in ("C1", "C2", "C3", "C4", "C5", "C6"):
KDS[mk] = "[M-4] 내면반지름 여유 거의 없음: 모델 내면지름 101.5 mm ↔ 3.5db 100.1/101.5 mm (H29, 표 4.5-1). 도면에 내면반지름 표기 없음."
for mk in ("C7", "C8"):
KDS[mk] = "[M-3] 굽힘 4곳이 각진 연결(내면반지름 미반영) → 내면지름 3.5db(H29 100.1 mm) 미적용 (KDS 24 14 21 표 4.5-1)."
KDS["P3"] = "[M-11] 갈고리 연장 모델 86.7 mm < 5db 95 mm (KDS 4.5.4), 내면지름 66.5 mm < 공칭 기준 66.85 mm (0.35 mm 부족)."
for mk in ("B1", "B2"):
KDS[mk] = "[M-10] S25 굽힘: 모델 내면지름 87.6 mm < 공칭 기준 88.9 mm (1.3 mm 부족, 3.5db)."
for mk in ("B3", "B7", "B10"):
KDS[mk] = "[M-10] S19 굽힘: 모델 내면지름 66.6 mm < 공칭 기준 66.9 mm (0.3 mm 부족, 3.5db)."
# =============================================================== 5. 표 행 만들기
HEAD = ["번 호", "형 식", "직 경", "개 수", "A", "B", "C", "D", "E", "R", "X1(º)", "X2(º)", "J", "길 이(M)", "총길이(M)", "단위중량(KG/M)", "총중량(TON)", "총중량(할증)(TON)"]
KEYS = ["num", "shape", "dia", "n", "A", "B", "C", "D", "E", "R", "X1", "X2", "J", "L", "tot", "unit", "w", "wh"]
def surch(d): return 1.06 if int(d) >= 22 else 1.03
def build_model_table():
"""도면 행 순서 그대로 모델 행 생성. 반환 {tab: [row dict]} (cells=표시 문자열, vals=비교용)"""
out = {}
for tab in ("SD400", "SD500"):
rows = []; grp = [] # 직전 소계 이후 막대 행들
last = {"shape": None, "dia": None}
for r in drow[tab]:
if r["kind"] == "bar":
mk = r["mark"]; m = mod.get(mk)
cells = {"num": mk}; vals = {}
dcells = r["cells"]
shp = m["shape"] if m else r["cells"]["shape_v"]
dia = m["dia"] if m else r["cells"]["dia_v"]
cells["shape"] = '"' if shp == last["shape"] else shp
cells["dia"] = '"' if dia == last["dia"] else dia
last["shape"], last["dia"] = shp, dia
if m:
cells["n"] = str(m["n"]); vals["n"] = m["n"]
c, v = model_cells(mk)
for k in DIMK:
cells[k] = c.get(k, ""); vals[k] = v.get(k)
cells["L"] = "%.3f" % (m["L"] / 1000.0); vals["L"] = m["L"]
cells["tot"] = "%.3f" % m["tot"]; vals["tot"] = m["tot"]
else:
cells["n"] = "0"; vals["n"] = 0
cells["L"] = ""; cells["tot"] = "0.000"; vals["tot"] = 0.0
for k in DIMK:
cells[k] = ""; vals[k] = None
rows.append({"kind": "bar", "mark": mk, "cells": cells, "vals": vals, "model": m, "dia_n": re.sub(r"\D", "", dia or ""), "dia": dia})
grp.append(rows[-1])
else:
lab = r["label"]; cells = {"num": lab}
dc = r["cells"]
if "소" in lab and "총" not in lab:
tl = sum(b["vals"]["tot"] for b in grp)
dn = grp[0]["dia_n"]
u = UNIT[dn]
w = tl * u / 1000.0; wh = w * surch(dn)
pc = "(%d%%)" % round((surch(dn) - 1) * 100)
cells.update(tot="%.3f" % tl, unit="%.3f" % u, w="%.3f" % w, wh="%.3f%s" % (wh, pc))
rows.append({"kind": "sum", "label": lab, "cells": cells, "vals": {"tot": tl, "w": round(w, 3), "wh": round(wh, 3)}, "dia_n": dn, "grp": list(grp)})
grp = []
elif "겹이음" in lab:
cells.update(w="", wh="")
rows.append({"kind": "lap", "label": lab, "cells": cells, "vals": {}})
else: # 총 계
subs = [x for x in rows if x["kind"] == "sum"]
tl = sum(s["vals"]["tot"] for s in subs)
w = sum(s["vals"]["w"] for s in subs); wh = sum(s["vals"]["wh"] for s in subs)
cells["w"] = "%.3f" % w; cells["wh"] = "%.3f" % wh
if dc.get("tot"):
cells["tot"] = "%.3f" % tl
rows.append({"kind": "tot", "label": lab, "cells": cells, "vals": {"tot": tl, "w": w, "wh": wh}})
out[tab] = rows
return out
MT = build_model_table()
# =============================================================== 6. 대조
def compare_row(dr, mr):
"""막대 행 비교 → {col: 'same'|'diff'|'round'|...}, 자동 비고 목록"""
res = {}; auto = []
mk = dr["mark"]; dc = dr["cells"]; mv = mr["vals"]; m = mr["model"]
# 개수
dn = int(dc["n"]) if dc["n"].isdigit() else None
if dn is None:
res["n"] = "drawing_blank"; auto.append("개수: 도면 빈칸 → 모델 %d" % mv["n"])
elif dn != mv["n"]:
res["n"] = "diff"; auto.append("개수: 도면 %d → 모델 %d (%+d)" % (dn, mv["n"], mv["n"] - dn))
else:
res["n"] = "same"
# 형식·직경
for k, mkey in (("shape", "shape"), ("dia", "dia")):
dv = dc[k + "_v"]; mvv = m[mkey] if m else None
res[k] = "same" if (mvv is None or dv == mvv) else "diff"
if mvv is not None and dv != mvv:
auto.append("%s: 도면 %s → 모델 %s" % ("형식" if k == "shape" else "직경", dv, mvv))
# 치수
for k in DIMK:
r_ = same(dc.get(k, ""), mv.get(k), k)
res[k] = r_
if r_ == "diff":
auto.append("%s: 도면 %s → 모델 %s" % (k, dc.get(k), mr["cells"][k]))
elif r_ == "drawing_blank":
auto.append("%s: 도면 빈칸 → 모델 %s" % (k, mr["cells"][k]))
elif r_ == "model_blank":
auto.append("%s: 도면 %s → 모델 미추출" % (k, dc.get(k)))
# 길이
dL = mm(dc["L"]); dL = dL[0] if dL else None
if m and dL is not None:
d = m["L"] - dL
if abs(d) <= TOL:
res["L"] = "same" if abs(d) < 0.5 else "round"
else:
res["L"] = "diff"; auto.append("길이 L: 도면 %.3f → 모델 %.3f m (%+.3f)" % (dL / 1000, m["L"] / 1000, d / 1000))
else:
res["L"] = "same" if dL is None and not m else "diff"
# 총길이
dT = float(dc["tot"]) if dc["tot"] else None
mT = mv["tot"]
if dT is not None:
d = mT - dT
tolt = max(dn or 0, mv["n"]) * TOL / 1000.0
if abs(d) <= 1e-9 + 0.0005:
res["tot"] = "same"
elif abs(d) <= tolt and res["L"] in ("same", "round"):
res["tot"] = "round"
else:
res["tot"] = "diff"; auto.append("총길이: 도면 %.3f → 모델 %.3f m (%+.3f)" % (dT, mT, d))
else:
res["tot"] = "same"
return res, auto
# =============================================================== 7. 엑셀
thin = Side(style="thin", color="999999")
BR = Border(left=thin, right=thin, top=thin, bottom=thin)
HF = PatternFill("solid", fgColor="D9D9D9")
GREY = PatternFill("solid", fgColor="EDEDED")
ORANGE = PatternFill("solid", fgColor="F8CBAD")
BLUE = PatternFill("solid", fgColor="DDEBF7")
YEL = PatternFill("solid", fgColor="FFF2CC")
GREEN = PatternFill("solid", fgColor="E2F0D9")
FILL = {"diff": ORANGE, "drawing_blank": YEL, "model_blank": YEL, "round": BLUE}
wb = Workbook()
def write_table(ws, title, tables, extra=None, cmp_mode=False):
ws.append([title])
ws.cell(row=1, column=1).font = Font(bold=True, size=12)
ws.append([])
heads = HEAD + (["판정", "비고 (자동: 칸별 차이)", "비고 (도면 내부·도면↔모델 코드)", "비고 3 (KDS 24 14 21 기준 상이)"] if cmp_mode else [])
hr = ws.max_row + 1
for tab_title, rows in tables:
ws.append([tab_title]); ws.cell(row=ws.max_row, column=1).font = Font(bold=True)
ws.append(heads); hr = ws.max_row
for c in range(1, len(heads) + 1):
cell = ws.cell(row=hr, column=c); cell.font = Font(bold=True); cell.fill = HF; cell.border = BR
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
for rr in rows:
ws.append(rr["line"])
r_ = ws.max_row
for c in range(1, len(heads) + 1):
cell = ws.cell(row=r_, column=c)
cell.border = BR
cell.alignment = Alignment(vertical="top", wrap_text=(c > 18), horizontal=("center" if c <= 18 else "left"))
for col, fill in rr.get("fills", {}).items():
ws.cell(row=r_, column=col).fill = fill
if rr.get("bold"):
for c in range(1, len(heads) + 1):
ws.cell(row=r_, column=c).font = Font(bold=True)
if c not in rr.get("fills", {}):
ws.cell(row=r_, column=c).fill = GREY
if cmp_mode:
ws.cell(row=r_, column=21).alignment = Alignment(vertical="top", wrap_text=True)
ws.cell(row=r_, column=22).font = Font(color="FF00FF")
ws.cell(row=r_, column=22).alignment = Alignment(vertical="top", wrap_text=True)
ws.append([])
widths = [10, 7, 7, 7, 10, 10, 10, 7, 7, 8, 8, 8, 6, 10, 11, 12, 11, 13]
if cmp_mode:
widths += [11, 60, 70, 52]
for i, w in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(i)].width = w
ws.freeze_panes = "B4"
def line_of(cells):
return [cells.get(k, "") for k in KEYS]
# ---- ① 도면 재료표 (재현) : 도면에 인쇄된 문자열 그대로
w1 = wb.active; w1.title = "①도면 재료표(재현)"
tabs1 = []
for tab, ttl in (("SD400", "철 근 재 료 표 (SD400) — 3.dxf 원본 값 그대로"), ("SD500", "철 근 재 료 표 (SD500) — 3.dxf 원본 값 그대로")):
rows = []
for r in drow[tab]:
if r["kind"] == "bar":
c = dict(r["cells"]); c["num"] = r["mark"]
rows.append({"line": line_of(c)})
else:
c = dict(r["cells"]); c["num"] = r["label"]
rows.append({"line": line_of(c), "bold": True})
tabs1.append((ttl, rows))
write_table(w1, "① 도면 재료표 재현 (운곡교 P1 완도방향, 도면 C0050205-027 3.dxf)", tabs1)
# ---- ② 모델 재료표
w2 = wb.create_sheet("②모델 재료표")
tabs2 = []
for tab, ttl in (("SD400", "철 근 재 료 표 (SD400) — 모델(운곡교p1.3dm) 기준"), ("SD500", "철 근 재 료 표 (SD500) — 모델 기준")):
rows = []
for r in MT[tab]:
c = dict(r["cells"])
if r["kind"] == "bar":
rows.append({"line": line_of(c)})
else:
c["num"] = r["label"]
rows.append({"line": line_of(c), "bold": True})
tabs2.append((ttl, rows))
write_table(w2, "② 모델 재료표 (모델 철근에서 추출: 개수·형상·치수 A~J·길이·총길이, 중량은 도면과 같은 단위중량·할증 적용)", tabs2)
# ---- ③ 대조·비고
w3 = wb.create_sheet("③대조·비고")
tabs3 = []
EXPORT = {"SD400": [], "SD500": []} # DXF 작성용 (make_bom_dxf.py 가 읽음)
stat = [] # 요약용
for tab, ttl in (("SD400", "SD400 — 모델 값 표시, 도면과 다른 칸은 색 + (도면: 값)"), ("SD500", "SD500 — 모델 값 표시, 도면과 다른 칸은 색 + (도면: 값)")):
rows = []
di = 0
for r in drow[tab]:
mr = MT[tab][di]; di += 1
if r["kind"] == "bar":
res, auto = compare_row(r, mr)
cells = dict(mr["cells"])
fills = {}
line = []
for ci, k in enumerate(KEYS, 1):
txt = cells.get(k, "")
if (k in res and res[k] not in ("same", "both_blank") and k not in ("shape", "dia")) or (k in ("shape", "dia") and res.get(k) == "diff"):
dt = r["cells"].get(k + "_v", r["cells"].get(k, "")) if k in ("shape", "dia") else r["cells"].get(k, "")
txt = "%s\n(도면: %s)" % (txt if txt != "" else "-", dt if dt != "" else "빈칸")
fills[ci] = FILL.get(res[k], ORANGE)
line.append(txt)
kind = [v for v in res.values()]
if "diff" in kind:
verdict = "차이"
elif "drawing_blank" in kind or "model_blank" in kind:
verdict = "도면 빈칸/모델 미추출"
elif "round" in kind:
verdict = "일치(반올림)"
else:
verdict = "일치"
fills[19] = {"차이": ORANGE, "도면 빈칸/모델 미추출": YEL, "일치(반올림)": BLUE, "일치": GREEN}[verdict]
line += [verdict, "\n".join(auto), NOTE.get(r["mark"], ""), KDS.get(r["mark"], "")]
rows.append({"line": line, "fills": fills})
EXPORT[tab].append({"kind": "bar", "mark": r["mark"], "d": {k: r["cells"].get(k, "") for k in KEYS},
"m": {k: mr["cells"].get(k, "") for k in KEYS}, "res": res, "verdict": verdict,
"auto": auto, "note": NOTE.get(r["mark"], ""), "kds": KDS.get(r["mark"], "")})
stat.append((r["mark"], verdict, auto))
else:
cells = dict(mr["cells"]); cells["num"] = r["label"]
fills = {}
line = []
for ci, k in enumerate(KEYS, 1):
txt = cells.get(k, ""); dt = r["cells"].get(k, "")
if k in ("tot", "w", "wh") and dt and txt:
mt_ = re.match(r"[\d.]+", txt); md_ = re.match(r"[\d.]+", dt)
if mt_ and md_ and abs(float(mt_.group()) - float(md_.group())) > 0.0015:
txt = "%s\n(도면: %s)" % (txt, dt); fills[ci] = ORANGE
line.append(txt)
extra = ""
if mr["kind"] == "lap":
extra = "도면 값(%s / %s)만 있음. 겹이음 할증 산식이 도면에 없어 모델은 산출하지 않음." % (r["cells"].get("w", ""), r["cells"].get("wh", ""))
line[KEYS.index("w")] = "(도면: %s)" % r["cells"].get("w", "")
line[KEYS.index("wh")] = "(도면: %s)" % r["cells"].get("wh", "")
line += ["", extra, "", ""]
rows.append({"line": line, "fills": fills, "bold": True})
sres = {KEYS[ci - 1]: "diff" for ci in fills}
EXPORT[tab].append({"kind": mr["kind"], "label": r["label"], "d": {k: r["cells"].get(k, "") for k in KEYS},
"m": {k: mr["cells"].get(k, "") for k in KEYS}, "res": sres, "verdict": "", "auto": [],
"note": extra, "kds": ""})
tabs3.append((ttl, rows))
write_table(w3, "③ 대조·비고 — 같은 양식(도면 표 순서)으로, 모델 값 위에 도면과 다른 칸을 표시", tabs3, cmp_mode=True)
for row in w3.iter_rows(min_row=4):
lines = 1
for c in row:
if c.value is None:
continue
s = str(c.value)
wd = w3.column_dimensions[get_column_letter(c.column)].width or 10
n = sum(max(1, int(len(p) * (1.7 if c.column > 19 else 1.0) / wd) + 1) if c.column > 19 else 1 for p in s.split("\n")) if c.column > 19 else len(s.split("\n"))
lines = max(lines, n)
w3.row_dimensions[row[0].row].height = max(15, 14 * lines)
w3.insert_rows(2)
w3.cell(row=2, column=1, value="색: 주황=도면과 다름 · 파랑=반올림 범위(≤1 mm/본) · 노랑=도면 빈칸/모델 미추출 · 판정 초록=일치 · 마젠타 글자=KDS 24 14 21 기준 상이. 코드 [M-n]=도면↔모델, [D-n]=도면 내부(검토기록_최종.md).")
w3.cell(row=2, column=1).font = Font(italic=True)
# ---- ④ 요약
w4 = wb.create_sheet("④요약")
w4.append(["도면 재료표 ↔ 모델 재료표 차이 요약 (2026-10-16, 검토 미확정)"])
w4.cell(row=1, column=1).font = Font(bold=True, size=12)
w4.append([])
w4.append(["마크", "판정", "차이 내용(자동 추출)"])
for c in range(1, 4):
w4.cell(row=3, column=c).font = Font(bold=True); w4.cell(row=3, column=c).fill = HF
for mk, v, auto in stat:
if v != "일치":
w4.append([mk, v, " / ".join(auto)])
w4.append([])
cnt = collections.Counter(v for _, v, _ in stat)
w4.append(["판정 집계: " + ", ".join("%s %d" % kv for kv in sorted(cnt.items()))])
for i, w in enumerate([10, 22, 150], 1):
w4.column_dimensions[get_column_letter(i)].width = w
# ---- ⑤ 코드 설명
w5 = wb.create_sheet("⑤비고코드설명")
w5.append(["코드", "구분", "내용"])
for c in [
("M-1", "도면↔모델", "C14 최하단 점 2개 모델이 350 mm 높음(근거 없음)"),
("M-2", "미확인", "C14 훅 방향 도면 표기 없음 (모델은 아래 가정)"),
("M-3", "기준↔모델", "C7·C8 굽힘 4곳 각진 연결 (내면반지름 미반영)"),
("M-4", "기준↔모델", "C1~C6 내면반지름 여유 거의 없음"),
("M-5", "도면↔모델", "C9~C13 길이 산식 +54.3 규칙이 변하는 치수에만 적용"),
("M-6", "도면↔모델", "S1·S2 높이 기준 (도면 대좌 상면 -50 ↔ 모델 코핑 상면 -50)"),
("M-7", "도면↔모델", "S1·S2 층 순서 (도면 S2 위 ↔ 모델 S1 위)"),
("M-8", "도면↔모델", "기초 교차 철근 간격 (도면 27~28 mm ↔ 모델 54~56 mm)"),
("M-9", "도면↔모델", "기초 B5 중심 73 아래(피복 58.5), B2·B6 다리 끝 높이"),
("M-10", "기준↔모델", "기초 S25·S19 굽힘 내면지름이 공칭 기준에 1.3/0.3 mm 부족"),
("M-11", "기준↔모델", "P3 갈고리 연장 86.7 < 5db 95, 내면지름 66.5 < 66.85"),
("M-12", "미확인", "P3 이음·P4/P5 커플러 각도 도면에 없음 (모델 가정)"),
("M-13", "도면↔모델", "UA1(73본, H22) 모델에 없음"),
("D-4/D-5", "도면 내부", "스터럽 폭 재료표 1.407 ↔ 단면 작도 1.160/1.740, 폐합근 1.106/1.686"),
("D-6", "도면 내부", "재료표 L 이 (최대+최소)/2 근사 → 총길이 과대"),
("D-7", "도면 내부", "C2·C5 L 12.081 ↔ 치수 합 12.082"),
("D-8", "도면 내부", "다리 각도 재료표 98°/96° ↔ 정면도 96.4°"),
("D-9", "도면 내부", "C7·C8 재료표 B·D·R·X·J 빈칸"),
("D-15~D-18", "도면 내부", "기초 점/선 높이 27~28 mm 차, B12 34↔28, B3 치수↔본수, B10 단 수"),
("D-20~D-23", "도면 내부", "P1/P2 상단 EL, 기둥 반지름, 링 R, P4·P5 @100↔@120"),
("E-01", "선행보고서", "기둥 띠철근 간격 → 구조계산서 확인 사항")]:
w5.append(list(c))
for i, w in enumerate([12, 14, 90], 1):
w5.column_dimensions[get_column_letter(i)].width = w
for c in range(1, 4):
w5.cell(row=1, column=c).font = Font(bold=True); w5.cell(row=1, column=c).fill = HF
# ---- ⑥ 개별 철근
w6 = wb.create_sheet("⑥모델_개별철근")
w6.append(["마크", "직경", "형식", "발생(occurrence)", "L(각진+커플러, mm)", "커플러(mm)", "중심선 실길이(mm)", "호스트", "근거(src)"])
for mk in sorted(mod, key=lambda s: (re.sub(r"\d", "", s), int(re.sub(r"\D", "", s) or 0))):
for b in sorted(mod[mk]["items"], key=lambda b: b["us"].get("occurrence", "")):
us = b["us"]
w6.append([mk, us.get("dia"), us.get("shape"), us.get("occurrence"), round(num(us.get("design_length")) + num(us.get("coupler_length")), 3),
num(us.get("coupler_length")), round(clen(b["g"], 800), 1), us.get("host"), us.get("src")])
for c in range(1, 10):
w6.cell(row=1, column=c).font = Font(bold=True); w6.cell(row=1, column=c).fill = HF
for i, w in enumerate([8, 7, 7, 18, 20, 11, 18, 10, 70], 1):
w6.column_dimensions[get_column_letter(i)].width = w
w6.freeze_panes = "A2"
wb.move_sheet("④요약", offset=-3)
import json, os
jp = os.path.join(os.path.dirname(OUT), "추출데이터", "재료표_대조결과.json")
json.dump(EXPORT, open(jp, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
try:
wb.save(OUT)
except PermissionError:
print("엑셀 파일이 열려 있어 저장하지 못함(JSON 만 갱신):", OUT)
print("saved", OUT)
print("판정:", dict(cnt))
for mk, v, auto in stat:
if v != "일치":
print(mk, v, " | ".join(auto))