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>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

@@ -0,0 +1,596 @@
# -*- 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))