Files
Pier_v1/검토파일/검토_산외교P1/스크립트/make_bom_compare_sw.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

597 lines
31 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 철근재료표 대조 (운곡교 make_bom_compare.py 를 산외교 도면·모델용으로 옮긴 것).
사용: python -I make_bom_compare_sw.py <pylib경로> <texts.json> <출력xlsx>
texts.json : 도면 DXF 의 글자 목록 (x, y, 레이어, 글자, 높이) - 같은 폴더 '추출데이터\도면_글자.json'
입력(고정): 산외교P1 G5.3dm (모델)
시트: ①도면 재료표(재현) ②모델 재료표 ③대조·비고 ④요약 ⑤비고코드설명 ⑥모델_개별철근
검토 범위: 모델↔도면. 도면 내부 불일치는 '도면내부_오류_보관.md' 에만 기록(여기 비고에는 넣지 않음).
"""
import sys, re, math, collections, json, os
sys.path.insert(0, sys.argv[1])
import rhino3dm
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
R = json.load(open(sys.argv[2], encoding="utf-8"))
OUT = sys.argv[3]
M3DM = r"D:\★Pier 자료\심수석님 검토 파일\검토_산외교P1\산외교P1 G5.3dm"
UNIT = {"32": 6.230, "29": 5.040, "25": 3.980, "22": 3.040, "19": 2.250, "16": 1.560, "13": 0.995}
TOL = 1.0 + 1e-6
# =============================================================== 1. 도면 재료표 파싱
COLS = [("num", 97500, 98300), ("shape", 98300, 99000), ("dia", 99000, 99800), ("n", 99800, 100600),
("A", 100600, 101800), ("B", 101800, 103000), ("C", 103000, 104300), ("D", 104300, 105500),
("E", 105500, 106600), ("R", 106600, 107600), ("X1", 107600, 108300), ("X2", 108300, 108900),
("X3", 108900, 109500), ("X4", 109500, 110100), ("J", 110100, 111300), ("L", 111300, 112600),
("tot", 112600, 113700), ("unit", 113700, 114700), ("w", 114700, 115800), ("wh", 115800, 117500)]
rowsraw = collections.defaultdict(lambda: collections.defaultdict(list))
for x, y, lay, t, h in R:
if not lay.startswith("CS-TABL") or not (-278800 < y < -265300):
continue
tab = "SD400" if y > -274000 else "SD500"
yy = round(y / 10) * 10
t = t.strip().replace("%%%", "%").replace("%%d", "°")
if 104000 <= x < 106000 and ("소계" in t or "총계" in t):
rowsraw[(tab, yy)]["label"].append(t)
continue
for name, lo, hi in COLS:
if lo <= x < hi:
rowsraw[(tab, yy)][name].append((x, t))
break
def first(c, k):
v = c.get(k)
return sorted(v)[0][1] if v else ""
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:
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}
d["shape_v"] = d["shape"]; d["dia_v"] = d["dia"]
drow[tab].append({"kind": "bar", "mark": num, "cells": d, "dia_n": re.sub(r"\D", "", d["dia"])})
DR = {r["mark"]: r for tab in drow for r in drow[tab] if r["kind"] == "bar"}
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
# =============================================================== 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 is_arc(s): return type(s).__name__ == "ArcCurve"
def clen(c, n=1500):
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 num(s):
try: return float(s)
except Exception: return 0.0
def lines_of(g):
return [(V(s.PointAtStart), V(s.PointAtEnd)) for s in segs_of(g) if is_line(s)]
def corner_seq(g):
"""직선 구간을 연장한 꼭짓점 사이 거리(각진 치수)."""
lines = lines_of(g)
if len(lines) < 2:
return [nrm(sub(l[1], l[0])) for l in lines]
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)))
seq = [lines[0][0]] + pts + [lines[-1][1]]
return [nrm(sub(seq[i + 1], seq[i])) for i in range(len(seq) - 1)]
def bar_dims(mk, shape, us, g):
"""한 철근의 재료표 항목(A..E, R, X1~X4) 을 mm / 도(°) 로. 모델에서 정의할 수 없는 항목(J 등)은 넣지 않는다."""
d = {}
segs = segs_of(g)
arcs = [s for s in segs if is_arc(s)]
seq = corner_seq(g)
if shape == "I":
if len(seq) == 1:
d["A"] = seq[0]
elif len(seq) == 3:
d["A"] = seq[1]; d["B"] = seq[0]
elif shape == "II": # C7~C9
d["C"] = seq[0]; d["B"] = seq[1]; d["A"] = seq[2]
elif shape == "III":
if mk == "C10": # 폐합 스터럽(변 2개씩)
d["B"] = seq[1]; d["A"] = seq[2]
else: # B9·B10
d["B"] = seq[0]; d["A"] = seq[1]
elif shape in ("VII", "VIII"): # C11·C13 갈고리 달린 수평근
ls = [s for s in segs if is_line(s)]
d["C"] = nrm(sub(V(ls[0].PointAtEnd), V(ls[0].PointAtStart)))
d["A"] = seq[2]
d["R"] = arcs[0].Arc.Radius - num("0")
elif shape == "XII": # C12 (R400 굽힘이 있어 도면 치수는 접선 직선 길이)
ln = [nrm(sub(l[1], l[0])) for l in lines_of(g)]
d["B"] = ln[1]; d["A"] = ln[2]
d["C"] = arc_len(arcs[0]); d["R"] = arcs[0].Arc.Radius
elif shape == "IX": # C1~C4
d["B"] = seq[0]; d["A"] = seq[1]; d["C"] = seq[2]
d["X1"] = 180.0 - arcs[0].Arc.AngleDegrees; d["X2"] = 180.0 - arcs[1].Arc.AngleDegrees
elif shape == "IV":
if mk in ("C5", "C6"):
d["A"], d["B"], d["C"], d["D"], d["E"] = seq[0], seq[1], seq[2], seq[3], seq[4]
for i, a in enumerate(arcs[:4]):
d["X%d" % (i + 1)] = 180.0 - a.Arc.AngleDegrees
else: # B2·B5
d["B"] = seq[0]; d["A"] = seq[1]
elif shape == "V":
if mk == "B8": # 기초 둘레 폐합근: 변 길이
d["A"] = seq[1]; d["B"] = seq[2]
else: # P3 이음 링 (반원)
d["A"] = arc_len(arcs[0]); d["B"] = num(us.get("coupler_length")); d["R"] = arcs[0].Arc.Radius
elif shape == "X": # P4 링
d["A"] = arc_len(arcs[0]); d["B"] = num(us.get("coupler_length")); d["R"] = arcs[0].Arc.Radius
elif shape == "VI":
big = max(arcs, key=lambda s: s.Arc.Radius)
d["R"] = big.Arc.Radius
if mk == "P1":
ls = [s for s in segs if is_line(s)]
lens = sorted(nrm(sub(V(s.PointAtEnd), V(s.PointAtStart))) for s in ls)
d["C"] = lens[0]; d["A"] = lens[-1]; d["B"] = arc_len(arcs[0])
return d
bars = collections.defaultdict(dict)
for o in m3.Objects:
L = lay.get(o.Attributes.LayerIndex, "")
g = o.Geometry; nm = o.Attributes.Name or ""
if isinstance(g, rhino3dm.Brep):
us = dict(o.Attributes.GetUserStrings())
if us.get("mark"):
bars[us["mark"]].setdefault(us.get("occurrence"), {}).update(us=us, name=nm, layer=L)
elif nm.startswith("P1-RC-"):
p = nm.split("-")
bars[p[2]].setdefault("-".join(p[3:]), {})["g"] = g
mod = {}
HALF = ("P2", "P3")
for mk, dd in bars.items():
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"], 800)
for k, v in bar_dims(mk, shape, us, b["g"]).items():
if v is not None:
dims[k].append(v)
half = mk in HALF
n = len(items) // 2 if half else len(items)
# 형상 길이 합(곡선 실길이) 과 design_length 의 차
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", "X3", "X4"):
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", "X3", "X4"):
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]]
return fmt_one(hi, key) + "~" + fmt_one(lo, key), [hi, lo]
def same(dtxt, mvals, key):
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", "X3", "X4"):
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", "X3", "X4", "J"]
# 재료표 치수 = 모델(작도) 치수 + 가산값 (주철근D+스터럽D 등 도면 산식). 일치하면 'rule' 로 표시
RULE = {"C10": {"A": 54, "B": 99}, "C12": {"A": 54, "B": 99}, "C11": {"A": 44}, "C13": {"A": 44}}
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", "C2", "C3", "C4"):
NOTE[mk] = "[M-1] 일부 철근 A 가 도면 단일값보다 짧음(모델은 철근마다 개별 길이). [M-2] J 1.560 은 재료표 L 에만 포함되고 모델 형상에는 작도되지 않음."
for mk in ("C5", "C6"):
NOTE[mk] = "[M-1] 일부 철근 B·D 가 도면 4.018/4.016 보다 짧음. [M-2] J 940 은 재료표 L 에만 포함, 모델 형상에는 없음."
for mk in ("C7", "C8", "C9"):
NOTE[mk] = "[M-3] 재료표 A·C·L 은 대푯값(a=평균+54), 모델은 철근마다 작도값 → 철근별로 다름(길이 합은 근사). [M-2] 재료표 L=2A+2B+2C 의 C 660 이 모델 폐합 스터럽 형상에는 없음."
NOTE["C10"] = "[M-3] 재료표 A·B 는 모델 작도값 +54/+99. [M-4] 재료표 A 최대 9.570 에 대응하는 철근이 모델에 없음(모델 최대 A 8.633+54=8.687, 설계길이 합 -6.2 m). [M-2] J 660 은 L 에만 반영."
NOTE["C11"] = "[M-3] 재료표 A = 모델 작도값 +44 (일치). B 165 는 모델에서 정의 불가(미추출)."
NOTE["C12"] = "[M-3] 재료표 A·B = 모델 작도값 +54/+99 (일치). [M-2] J 660 은 L 에만 반영."
NOTE["C13"] = "[M-3] 재료표 A = 모델 작도값 +44 (일치). B 165 미추출. [M-2] J 660 은 L 에만 반영."
NOTE["C14"] = "[M-1] 재료표 A 3.000 단일 ↔ 모델 일부 철근 A 2.633~2.892(짧음)."
NOTE["C15"] = "[M-1] 재료표 A 4.500 단일 ↔ 모델 A 4.118/4.489/4.517."
NOTE["P2"] = "[M-2] A·B·J 는 모델에서 정의 불가(미추출). 모델은 반링 2조각(140개)=1본 → 개수 70 으로 환산."
NOTE["P3"] = "모델은 반원 2조각(24개)=1링 → 개수 12 로 환산."
NOTE["P1"] = "[M-6] 도면 표의 주철근 커플러 476개(H32)가 모델에 없음 — 모델 P1 은 커플러 없이 연속 28.545 m 로 작도(68본×7 = 476 로 추정)."
NOTE["B2"] = "[M-2] J 1.000 은 L 에만 반영, 모델 형상에는 없음(모델 중심선 11.758 < L 12.800)."
NOTE["B5"] = NOTE["B2"]
NOTE["B8"] = "[M-5] 모델은 정사각 7.345×7.345, 재료표 A 7.300 · B 7.350. [M-2] J 570 은 L 에만 반영(모델 29.339 < L 31.581)."
KDS["P5"] = "[M-7] H19 굽힘 내면지름 66.5 mm < 공칭(19.1) 3.5db 66.85 mm (0.35 mm 부족)."
for mk in ("B2", "B5", "C7", "C8", "C9", "C10", "C15"):
KDS[mk] = "[M-7] H22 굽힘 내면지름 76.8 mm < 공칭(22.2) 3.5db 77.7 mm (0.9 mm 부족)."
NOTE["P3"] = NOTE["P3"] + " [M-8] 이음(커플러 55) 각도 90°/270° 는 도면에 기준이 없어 모델 가정."
NOTE["P4"] = "[M-8] 커플러(55) 각도 90° 는 도면에 기준이 없어 모델 가정."
# =============================================================== 5. 표 행 만들기
HEAD = ["번 호", "형 식", "직 경", "개 수", "A", "B", "C", "D", "E", "R", "X1(º)", "X2(º)", "X3(º)", "X4(º)", "J", "길 이(M)", "총길이(M)", "단위중량(KG/M)", "총중량(TON)", "총중량(할증)(TON)"]
KEYS = ["num", "shape", "dia", "n", "A", "B", "C", "D", "E", "R", "X1", "X2", "X3", "X4", "J", "L", "tot", "unit", "w", "wh"]
NK = len(KEYS)
def surch(d): return 1.06 if int(d) >= 22 else 1.03
def build_model_table():
out = {}
for tab in ("SD400", "SD500"):
rows = []; grp = []
for r in drow[tab]:
if r["kind"] == "bar":
mk = r["mark"]; m = mod.get(mk)
cells = {"num": mk}; vals = {}
shp = m["shape"] if m else r["cells"]["shape_v"]
dia = m["dia"] if m else r["cells"]["dia_v"]
cells["shape"] = shp; cells["dia"] = 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}
if "소계" 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 = []
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.update(tot="%.3f" % tl, w="%.3f" % w, wh="%.3f" % wh)
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):
res = {}; auto = []
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:
if k == "J":
# J 는 모델 형상에서 정의되지 않음 (L 에만 반영)
res[k] = "model_blank" if dc.get("J") else "both_blank"
if dc.get("J"):
auto.append("J: 도면 %s → 모델 미작도(L 에만 반영)" % dc["J"])
continue
r_ = same(dc.get(k, ""), mv.get(k), k)
off = RULE.get(dr["mark"], {}).get(k)
if r_ == "diff" and off and mv.get(k):
r2 = same(dc.get(k, ""), [v + off for v in mv[k]], k)
if r2 == "same":
r_ = "rule"
auto.append("%s: 도면 %s = 모델 %s + %d (가산 규칙)" % (k, dc.get(k), mr["cells"][k], off))
res[k] = r_
if r_ == "rule":
pass
elif 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) <= 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"
# 형상 실길이(중심선) 대비: 재료표 L 은 각진 치수 합이라 굽힘 때문에 약간 짧은 것은 정상. 큰 차이(>3%)만 기록
if m and dL is not None and dn:
clm = m["cl"] * 1000.0 / m["n"]
if dL - clm > 0.03 * dL:
auto.append("형상 중심선 평균 %.3f m < 재료표 L %.3f m (%.0f mm 짧음, 굽힘 감소 제외 시 J 등 미작도 의심)" % (clm / 1000, dL / 1000, dL - clm))
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, "rule": BLUE}
wb = Workbook()
VCOL = NK + 1 # 판정 열 번호
def write_table(ws, title, tables, 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 [])
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 > NK), horizontal=("center" if c <= NK 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=NK + 4).font = Font(color="FF00FF")
ws.append([])
widths = [8, 6, 6, 6, 10, 10, 10, 7, 7, 8, 7, 7, 7, 7, 8, 10, 11, 11, 10, 12]
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 in ("SD400", "SD500"):
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(("철 근 재 료 표 (%s) — 도면 원본 값 그대로" % tab, rows))
write_table(w1, "① 도면 재료표 재현 (산외교 울산 P1, 교각배근도(울산P1)_2016.10.28)", tabs1)
w2 = wb.create_sheet("②모델 재료표")
tabs2 = []
for tab in ("SD400", "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(("철 근 재 료 표 (%s) — 모델(산외교P1 G5.3dm) 기준" % tab, rows))
write_table(w2, "② 모델 재료표 (모델 철근에서 추출: 개수·형상·치수·길이·총길이, 중량은 도면과 같은 단위중량·할증 적용)", tabs2)
w3 = wb.create_sheet("③대조·비고")
tabs3 = []
EXPORT = {"SD400": [], "SD500": []}
stat = []
for tab in ("SD400", "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 = list(res.values())
if "diff" in kind:
verdict = "차이"
elif "drawing_blank" in kind or "model_blank" in kind:
verdict = "도면 빈칸/모델 미추출"
elif "round" in kind or "rule" in kind:
verdict = "일치(반올림)"
else:
verdict = "일치"
fills[VCOL] = {"차이": 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)
line += ["", "", "", ""]
rows.append({"line": line, "fills": fills, "bold": True})
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": {KEYS[ci - 1]: "diff" for ci in fills},
"verdict": "", "auto": [], "note": "", "kds": ""})
tabs3.append(("%s — 모델 값 표시, 도면과 다른 칸은 색 + (도면: 값)" % tab, 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 / wd) + 1) for p in s.split("\n")) if c.column > NK 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]=도면↔모델(⑤ 참고). 도면 내부 불일치는 별도 보관(도면내부_오류_보관.md).")
w3.cell(row=2, column=1).font = Font(italic=True)
w4 = wb.create_sheet("④요약")
w4.append(["도면 재료표 ↔ 모델 재료표 차이 요약 (검토 미확정, 자동 비교)"]); 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, 160], 1):
w4.column_dimensions[get_column_letter(i)].width = w
CODES = [
("M-1", "도면↔모델", "C1~C6·C14·C15 일부 철근이 재료표 단일값보다 짧음(C1 A 8.462~9.259, C5 B 3.580~4.018, C14 A 2.633~3.000, C15 A 4.118~4.517) — 모델은 철근마다 개별 길이"),
("M-2", "도면↔모델", "J·C 길이(C1~C4 J 1.560, C5·C6 J 940, C7~C9 C 660×2, C10·C12·C13 J 660, P2 J 380, B2·B5 J 1.000, B8 J 570)가 재료표 L 에만 있고 모델 형상에는 작도되지 않음"),
("M-3", "도면↔모델", "C7~C13 치수 산식(+54/+44/+99 가산): 재료표는 대푯값·가산값, 모델은 철근마다 작도값"),
("M-4", "도면↔모델", "C10 재료표 A 최대 9.570 에 대응하는 철근이 모델에 없음(모델 A 최대 8.687)"),
("M-5", "도면↔모델", "B8 모델 정사각 7.345×7.345 ↔ 재료표 A 7.300·B 7.350"),
("M-6", "도면↔모델", "주철근 커플러 476개(H32, 도면 표)가 모델에 없음 — P1 은 연속 철근으로 작도"),
("M-7", "기준↔모델", "굽힘 내면지름: H22(B2·B5·C7~C10·C15) 76.8 < 77.7, H19(P5) 66.5 < 66.85 — KDS 24 14 21 표 4.5-1 3.5db 에 0.9/0.35 mm 부족"),
("M-8", "미확인", "P3 이음·P4 커플러 각도(90°/270°) 도면에 기준 없음, 모델 가정"),
]
w5 = wb.create_sheet("⑤비고코드설명")
w5.append(["코드", "구분", "내용"])
for c in CODES:
w5.append(list(c))
for i, w in enumerate([10, 14, 120], 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)", "호스트"])
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"], 600), 1), us.get("host")])
for c in range(1, 9):
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, 20, 22, 11, 18, 10], 1):
w6.column_dimensions[get_column_letter(i)].width = w
w6.freeze_panes = "A2"
wb.move_sheet("④요약", offset=-3)
jp = os.path.join(os.path.dirname(os.path.dirname(OUT)) if False else os.path.dirname(OUT), "추출데이터", "재료표_대조결과.json")
os.makedirs(os.path.dirname(jp), exist_ok=True)
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))