# -*- coding: utf-8 -*- """산외교(울산) P1 철근재료표 대조 (운곡교 make_bom_compare.py 를 산외교 도면·모델용으로 옮긴 것). 사용: python -I make_bom_compare_sw.py <출력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))