# -*- coding: utf-8 -*- """운곡교 P1 철근재료표 대조 (도면 재료표와 같은 양식으로 도면표 재현 → 모델표 작성 → 대조·비고). 사용: python -I make_bom_compare.py <출력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))