Files
Pier_v1/Code/review_pipeline/dots_method/combine4.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

284 lines
20 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.
import sys, json, os
SCR = r"C:\Users\User\AppData\Local\Temp\claude\D--Rhino-MCP-rhino-mcp-main\f46b47bc-9105-4079-9b62-fcbee31b26ae\scratchpad"
sys.path.insert(0, SCR + r"\pylib")
import openpyxl
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.drawing.image import Image as XImage
from openpyxl.utils import get_column_letter
from PIL import Image
ROOT = r"D:\★Pier 자료\심수석님 검토 파일"
OUT = ROOT + r"\4개교_공통사항_종합.xlsx"
TMP = SCR + r"\c4_imgs"
os.makedirs(TMP, exist_ok=True)
BR = {"운곡교": ROOT + r"\검토_운곡교P1\도면↔모델_불일치_시각자료", "산외교": ROOT + r"\검토_산외교P1\도면↔모델_불일치_시각자료",
"옥성교": ROOT + r"\검토_옥성교P1\도면↔모델_불일치_시각자료", "Ramp-A교": ROOT + r"\Ramp-A교P1\도면↔모델_불일치_시각자료"}
BRIDGES = ["운곡교", "산외교", "옥성교", "Ramp-A교"]
ug = json.load(open(SCR + r"\ug_items.json", encoding="utf-8"))
sw = json.load(open(SCR + r"\sw_cfg.json", encoding="utf-8"))["items"]
ok = json.load(open(SCR + r"\ok_cfg.json", encoding="utf-8"))["items"]
ra = json.load(open(SCR + r"\ra_cfg.json", encoding="utf-8"))["items"]
ITEMS = {"운곡교": ug, "산외교": sw, "옥성교": ok, "Ramp-A교": ra}
# 항목 → 공통 문제 번호
MAP = {
"운곡교": {"M-1": "개별", "M-2": "개별", "M-3": "C-1", "M-4": "C-1", "M-5": "C-4", "M-6": "개별", "M-7": "개별", "M-8": "C-8", "M-9": "C-8", "M-10": "C-1", "M-11": "C-1", "M-12": "C-2", "M-13": "C-7"},
"산외교": {"M-1": "C-5", "M-2": "C-3", "M-3": "C-4", "M-4": "C-5", "M-5": "개별", "M-6": "C-6", "M-7": "C-1", "M-8": "C-2", "M-9": "C-8", "M-10": "개별"},
"옥성교": {"M-1": "C-7", "M-2": "C-3", "M-3": "개별", "M-4": "C-4", "M-5": "C-4", "M-6": "C-5", "M-7": "C-1", "M-8": "C-2", "M-9": "C-8", "M-10": "개별", "M-11": "개별", "M-12": "C-8"},
"Ramp-A교": {"M-1": "C-3", "M-2": "C-4", "M-3": "C-5", "M-4": "C-5", "M-5": "C-6", "M-6": "C-1", "M-7": "C-2"},
}
thin = Side(style="thin", color="999999")
B = Border(left=thin, right=thin, top=thin, bottom=thin)
HF = PatternFill("solid", fgColor="D9D9D9")
FILL = {"도면↔모델": PatternFill("solid", fgColor="F8CBAD"), "기준↔모델": PatternFill("solid", fgColor="E6C84A"), "미확인": PatternFill("solid", fgColor="D9D9D9"), "일치": PatternFill("solid", fgColor="E2F0D9")}
YES = PatternFill("solid", fgColor="F8CBAD")
REL = PatternFill("solid", fgColor="FFF2CC")
TOP = Alignment(wrap_text=True, vertical="top")
CEN = Alignment(wrap_text=True, vertical="center", horizontal="center")
COMMON = [
("C-1", "굽힘 내면지름이 KDS 24 14 21 (3.5db) 에 미달", {"운곡교": "M-3·M-4·M-10·M-11", "산외교": "M-7", "옥성교": "M-7", "Ramp-A교": "M-6"}, "확정",
"H19 내면지름 66.5 < 66.85 (3교 같은 값), H22 76.8 < 77.7 (산외·옥성 같은 값), H25 87.0 < 88.9 (옥성·Ramp-A 같은 값), 운곡교 S25 87.6·S19 66.6, P3 갈고리 66.5",
"모델 생성 규칙의 굽힘 반지름이 프로젝트와 무관하게 같은 값 (H19 R 42.8, H22 R 49.5, H25 R 56.2 — 공칭 db 의 2.2~2.25배 반지름) → 규칙 하나 때문으로 추정",
"굽힘 R 규칙을 KDS 기준(내면지름 ≥ 3.5db → 중심선 R ≥ 2.25db 이상)으로 바꿀지 결정. 도면에는 반지름 표기가 없음"),
("C-2", "이음·커플러·갈고리 끝 각도/위치가 도면에 없어 모델이 임의로 가정", {"운곡교": "M-12", "산외교": "M-8", "옥성교": "M-8", "Ramp-A교": "M-7"}, "확정",
"P3 이음 엇갈림 90°/270°, P4·P5 커플러 위치, 링 이음 j 위치 (4교 모두 같은 가정)",
"도면이 형상 도해에 커플러 위치만 보이고 단마다 엇갈림 각도를 적지 않음 → 모델 생성기가 기본 각도(90°/270°)를 사용",
"설계자에게 각도 기준 확인 (도면 보완 요청 대상)"),
("C-3", "겹침·갈고리 길이(j, c)가 재료표 L 에만 있고 모델 형상에는 없음", {"운곡교": "관련 M-11(갈고리 연장)", "산외교": "M-2", "옥성교": "M-2", "Ramp-A교": "M-1"}, "확정 3교",
"산외 J 1.560(C1~C4)·0.660(C10) 등, 옥성 j 1.510(C1~C7)·0.560(B10~B12) 등 합 약 600 m, Ramp-A c 450/350·j 290. 일부는 설계길이에는 포함·형상에서만 빠짐, 옥성 C1~C7 은 설계길이에서도 제외",
"도면은 '이음 길이/겹침'을 L 에 더하지만 위치를 그리지 않음 → 모델이 기하(형상)로는 만들지 않고 설계길이 속성에만 반영",
"겹침·갈고리를 형상에 넣을지 가산 길이로만 둘지 정책 결정 (옥성 C1~C7 설계길이 누락은 별도 확인)"),
("C-4", "재료표 치수 = 모델 작도값 + 가산 규칙(주철근D + 스터럽D 등)", {"운곡교": "M-5", "산외교": "M-3", "옥성교": "M-4·M-5", "Ramp-A교": "M-2"}, "확정",
"운곡 +54.3 (C9~C13, 폭에는 미적용), 산외 +54/+44/+99, 옥성 a +46 / C14 a -54, Ramp-A a +48",
"도면 치수는 가산이 포함된 값, 모델은 작도 치수 → 같은 부재의 치수 정의가 달라 항상 수십 mm 차이가 남",
"가산 규칙과 적용 범위(길이/폭)를 확정하고 모델 치수 정의를 통일 (운곡교 M-5 와 같은 결정)"),
("C-5", "재료표·철근상세는 대표값/단일값, 모델은 철근마다 개별 길이 → 총길이 차이", {"운곡교": "관련 M-5(도면 내부 D-6)", "산외교": "M-1·M-4", "옥성교": "M-6", "Ramp-A교": "M-3·M-4"}, "확정 3교",
"산외 C1~C6·C14·C15 일부 짧음(8.462~9.259), 옥성 C18 a 3.050 ↔ 3종·C8~C13 총길이 -259 m, Ramp-A C8 -37.7 m·C12 +17.0 m (총 -49.9 m)",
"도면은 마크당 값 1개(또는 b 범위의 평균으로 L 계산), 모델은 코핑 하면 형상에 맞춘 개별 길이",
"철근별 개별 길이가 의도인지 확인, 재료표 총길이를 어느 기준으로 볼지 결정"),
("C-6", "커플러 수량 표의 커플러가 모델에 없음 (연속 철근으로 작도)", {"운곡교": "-", "산외교": "M-6", "옥성교": "미확인(표 대조 안 함)", "Ramp-A교": "M-5"}, "확정 2교",
"산외 주철근 커플러 476개, Ramp-A 115개(P1 54 + P1-1 61). 옥성 표(주철근 120·띠철근 58)는 대조 못 함",
"모델이 주철근을 한 개의 긴 직선 철근으로 만들고 커플러 객체·위치를 만들지 않음 (P1 은 연결 간격 길이만 반영)",
"커플러 위치·개수를 모델에 반영할지 판단, 옥성교 표 대조 필요"),
("C-7", "기초 조립용 철근(UA1/UM)이 모델에 없음", {"운곡교": "M-13", "산외교": "-", "옥성교": "M-1", "Ramp-A교": "-"}, "확정 2교",
"운곡 UA1 H22 73본 377.848 m, 옥성 UM H22 92본 414.368 m",
"도면의 '기초 조립용 철근 상세'는 배치 예시도(S=NONE)라 위치·간격이 없어 모델이 생략",
"조립용 철근을 모델에 넣을지 판단 (수량 근거는 재료표 뿐)"),
("C-8", "기초 교차 철근: 도면은 같은 높이(상면 100 · 하면 150)에 그렸는데 모델은 위·아래로 어긋나게 배치 (어긋나게 배치하라는 지시 없음 → 모델 쪽 오류)", {"운곡교": "M-8·M-9", "산외교": "M-9", "옥성교": "M-9·M-12", "Ramp-A교": "해당 없음(말뚝 기초)"}, "확정 3교",
"운곡 B5 73·B6 다리 끝 72·B2 128 (도면 100, ±27~28), 산외 B1~B6 77.5/122.5/127.5/172.5 (±22.5), 옥성 B5~B8 122.5/77.5/172.5 (±22.5, B1~B4 는 일치)",
"모델 생성 시 교차하는 철근끼리 겹치지 않게 한 철근 지름 정도씩 어긋나게 배치한 것으로 추정 (사용자 확인: 그렇게 하라는 지시는 한 적 없음)",
"모델 철근을 도면 선 높이에 맞출지 결정 — 맞추면 교차 철근 간격(운곡 M-8)도 같이 바뀜"),
("C-9", "선행 자동 검사(G1~G5)는 전부 PASS였지만 재료표 치수·형상 대조는 없었음", {"운곡교": "-", "산외교": "수량만 확인", "옥성교": "재료표 미인식", "Ramp-A교": "P1 만 인식"}, "프로세스 관찰 3교",
"산외 '자동으로 찾은 항목이 없습니다', 옥성·Ramp-A 도 같음 — 이번 대조에서 M-n 이 모두 나옴",
"선행 검사는 본수·고정 값 중심이고 도면 표의 치수/길이/커플러/형상 정의는 확인 범위 밖",
"선행 검사에 재료표 길이·치수·커플러 대조를 추가할지 검토"),
]
wb = Workbook()
# ------------------------------------------------ 안내
ws = wb.active
ws.title = "안내"
ws.append(["4개 교량 도면 ↔ 라이노 모델 검토 종합 — 공통 문제 정리"])
ws["A1"].font = Font(bold=True, size=14)
ws.append(["대상: 운곡교 P1 · 산외교(울산) P1 · 옥성교 P1 · Ramp-A교 P1 (작성 2026-10-19, 검토 미확정 초안). 기준 KDS 24 14 21. 범위는 모델↔도면이며 도면 내부 불일치는 이 표에서 뺐다(교량별 보관 파일)."])
ws["A2"].alignment = TOP
ws.merge_cells("A2:G2")
ws.row_dimensions[2].height = 48
ws.append([])
ws.append(["시트", "내용"])
for c in (1, 2):
ws.cell(row=4, column=c).font = Font(bold=True)
ws.cell(row=4, column=c).fill = HF
for a, b in [("공통 문제 요약", "4개 교량에서 겹치거나 반복된 문제 9가지(C-1~C-9) — 교량별 해당 코드, 대표 수치, 추정 원인, 조치 방향"),
("공통 문제 상세(이미지)", "공통 문제별로 교량별 캡처 이미지를 나란히 (도면 ↔ 모델)"),
("교량별 항목 합본", "4개 교량의 모든 불일치 항목 38건 + 어느 공통 문제에 해당하는지"),
("개별 항목", "한 교량에서만 나온 항목 (공통 문제가 아닌 것)"),
("교량 개요", "교량별 구조·검토 범위·본수 일치 여부·파일 위치")]:
ws.append([a, b])
ws.append([])
ws.append(["색: 주황=도면↔모델 / 진한 노랑=기준(KDS)↔모델 / 회색=미확인 · 공통 문제 표의 주황 칸=해당 교량 확정, 연노랑 칸=관련/미검토"])
ws.column_dimensions["A"].width = 26
ws.column_dimensions["B"].width = 120
# ------------------------------------------------ 공통 문제 요약
w1 = wb.create_sheet("공통 문제 요약")
w1.append(["공통 문제 요약 — 4개 교량에서 겹쳐 나온 문제"])
w1["A1"].font = Font(bold=True, size=13)
w1.append(["공통 정도 열: '확정'은 해당 교량 모두에서 확인, '3교/2교'는 그만큼 확인되고 나머지는 해당 없음, '미검토'는 아직 확인 안 함. 추정 원인은 모델 생성 방식에 대한 추정이며 확정이 아님."])
w1["A2"].alignment = TOP
w1.merge_cells("A2:K2")
w1.row_dimensions[2].height = 36
w1.append([])
H = ["번호", "공통 문제", "공통 정도"] + BRIDGES + ["대표 수치·근거", "추정 원인", "확인·조치 방향"]
w1.append(H)
for c in range(1, len(H) + 1):
cell = w1.cell(row=4, column=c)
cell.font = Font(bold=True)
cell.fill = HF
cell.border = B
cell.alignment = CEN
for no, title, br, deg, ev, cause, act in COMMON:
w1.append([no, title, deg] + [br[b] for b in BRIDGES] + [ev, cause, act])
r = w1.max_row
for c in range(1, len(H) + 1):
cell = w1.cell(row=r, column=c)
cell.border = B
cell.alignment = TOP
w1.cell(row=r, column=1).font = Font(bold=True)
for k, b in enumerate(BRIDGES):
v = br[b]
cell = w1.cell(row=r, column=4 + k)
cell.alignment = CEN
if v.startswith("M-"):
cell.fill = YES
elif v not in ("-",):
cell.fill = REL
w1.row_dimensions[r].height = 118
for i, w in enumerate([7, 34, 13, 15, 12, 14, 14, 44, 44, 40], 1):
w1.column_dimensions[get_column_letter(i)].width = w
w1.freeze_panes = "C5"
# ------------------------------------------------ 공통 문제 상세(이미지)
w2 = wb.create_sheet("공통 문제 상세(이미지)")
w2.append(["공통 문제 상세 — 교량별 캡처 (왼쪽 도면 / 오른쪽 모델, 각 이미지는 해당 교량 M-n 이미지)"])
w2["A1"].font = Font(bold=True, size=13)
w2.append([])
w2.append(["번호", "공통 문제"] + BRIDGES)
for c in range(1, 7):
w2.cell(row=3, column=c).font = Font(bold=True)
w2.cell(row=3, column=c).fill = HF
w2.cell(row=3, column=c).border = B
w2.cell(row=3, column=c).alignment = CEN
IMG = {
"C-1": {"운곡교": "M-10_기초_굽힘내면반지름.png", "산외교": "M-7_굽힘내면지름.png", "옥성교": "M-7_굽힘내면지름.png", "Ramp-A교": "M-6_굽힘내면지름.png"},
"C-2": {"운곡교": "M-12_기둥_이음커플러각도.png", "산외교": "M-8_P3P4_이음각도.png", "옥성교": "M-8_P3P4_이음각도.png", "Ramp-A교": "M-7_P2P3_갈고리겹침.png"},
"C-3": {"운곡교": "M-11_P3_갈고리_도면.png", "산외교": "M-2_J길이_미작도.png", "옥성교": "M-2_j길이_미작도.png", "Ramp-A교": "M-1_C7-C14_겹침c_j.png"},
"C-4": {"운곡교": "M-5_C9-C13_길이산식.png", "산외교": "M-3_C7-C13_가산산식.png", "옥성교": "M-4_C8-C13_스터럽.png", "Ramp-A교": "M-2_스터럽폭_가산48.png"},
"C-5": {"산외교": "M-1_일부철근짧음.png", "옥성교": "M-6_C18_단일값.png", "Ramp-A교": "M-3_C8-C14_단일값_총길이.png"},
"C-6": {"산외교": "M-6_주철근커플러476.png", "Ramp-A교": "M-5_커플러115.png"},
"C-7": {"운곡교": "M-13_UA1_모델누락.png", "옥성교": "M-1_UM_조립철근누락.png"},
"C-8": {"운곡교": "M-9_기초_상면피복.png", "산외교": "M-9_기초_철근중심높이.png", "옥성교": "M-9_기초_철근중심높이.png"},
}
IH = 300
for no, title, br, deg, ev, cause, act in COMMON:
if no not in IMG:
continue
w2.append([no, title])
r = w2.max_row
for c in range(1, 7):
cell = w2.cell(row=r, column=c)
cell.border = B
cell.alignment = TOP
w2.cell(row=r, column=1).font = Font(bold=True)
w2.row_dimensions[r].height = IH * 0.75 + 8
for k, b in enumerate(BRIDGES):
fn = IMG[no].get(b)
if not fn:
w2.cell(row=r, column=3 + k, value="(해당 없음 / 이미지 없음)").alignment = CEN
continue
src = BR[b] + "\\" + fn
if not os.path.exists(src):
w2.cell(row=r, column=3 + k, value="이미지 없음: " + fn)
continue
im = Image.open(src).convert("RGB")
h = IH
w = int(im.width * h / im.height)
tmp = TMP + "\\" + no + "_" + b + ".png"
im.resize((w, h), Image.LANCZOS).save(tmp)
xi = XImage(tmp)
xi.width, xi.height = w, h
w2.add_image(xi, get_column_letter(3 + k) + str(r))
w2.column_dimensions[get_column_letter(3 + k)].width = max(w2.column_dimensions[get_column_letter(3 + k)].width or 0, w / 7.0)
w2.column_dimensions["A"].width = 7
w2.column_dimensions["B"].width = 30
w2.freeze_panes = "C4"
# ------------------------------------------------ 교량별 항목 합본
w3 = wb.create_sheet("교량별 항목 합본")
w3.append(["교량별 불일치 항목 합본 (도면 내부 불일치 제외)"])
w3["A1"].font = Font(bold=True, size=13)
w3.append([])
H3 = ["교량", "코드", "마크", "내용", "비교", "차이 크기", "도면 쪽 상태", "확인·조치 필요 사항", "구분", "공통 문제"]
w3.append(H3)
for c in range(1, len(H3) + 1):
cell = w3.cell(row=3, column=c)
cell.font = Font(bold=True)
cell.fill = HF
cell.border = B
cell.alignment = CEN
for b in BRIDGES:
for it in ITEMS[b]:
cm = MAP[b].get(it["code"], "개별")
w3.append([b, it["code"], it["mark"], it["content"], it.get("cmp"), it.get("diff"), it.get("drawing_side"), it.get("action"), it["kind"], cm])
r = w3.max_row
for c in range(1, len(H3) + 1):
cell = w3.cell(row=r, column=c)
cell.border = B
cell.alignment = TOP
w3.cell(row=r, column=9).fill = FILL.get(it["kind"], FILL["미확인"])
if cm != "개별":
w3.cell(row=r, column=10).fill = YES
w3.row_dimensions[r].height = 62
for i, w in enumerate([10, 7, 14, 60, 24, 28, 34, 42, 11, 10], 1):
w3.column_dimensions[get_column_letter(i)].width = w
w3.freeze_panes = "D4"
w3.auto_filter.ref = "A3:J%d" % w3.max_row
# ------------------------------------------------ 개별 항목
w4 = wb.create_sheet("개별 항목")
w4.append(["한 교량에서만 나온 항목 (공통 문제에 해당하지 않음)"])
w4["A1"].font = Font(bold=True, size=13)
w4.append([])
w4.append(["교량", "코드", "마크", "내용", "차이 크기", "구분", "비고"])
for c in range(1, 8):
cell = w4.cell(row=3, column=c)
cell.font = Font(bold=True)
cell.fill = HF
cell.border = B
NOTE = {("옥성교", "M-3"): "C7 바깥 2본 형상 다름 — 모델 생성 방식 문제로 보임", ("산외교", "M-5"): "B8 정사각 7.345 vs 7.300×7.350",
("운곡교", "M-1"): "C14 최하단 점 높이 — 근거 기록 없음", ("운곡교", "M-2"): "C14 훅 방향 도면 표기 없음 (미확인)",
("운곡교", "M-6"): "S1·S2 높이 기준 (받침 쪽)", ("운곡교", "M-7"): "S1·S2 층 순서"}
for b in BRIDGES:
for it in ITEMS[b]:
if MAP[b].get(it["code"], "개별") == "개별":
w4.append([b, it["code"], it["mark"], it["content"], it.get("diff"), it["kind"], NOTE.get((b, it["code"]), "")])
r = w4.max_row
for c in range(1, 8):
w4.cell(row=r, column=c).border = B
w4.cell(row=r, column=c).alignment = TOP
w4.cell(row=r, column=6).fill = FILL.get(it["kind"], FILL["미확인"])
w4.row_dimensions[r].height = 52
for i, w in enumerate([10, 7, 16, 70, 24, 11, 40], 1):
w4.column_dimensions[get_column_letter(i)].width = w
# ------------------------------------------------ 교량 개요
w5 = wb.create_sheet("교량 개요")
w5.append(["교량", "구조·특징", "검토 마크 수", "본수 일치", "불일치 항목 수", "도면 형식", "검토 범위(완료/미실시)", "폴더"])
for c in range(1, 9):
w5.cell(row=1, column=c).font = Font(bold=True)
w5.cell(row=1, column=c).fill = HF
w5.cell(row=1, column=c).border = B
rows5 = [
["운곡교 P1", "2주식, 코핑 C1~C16·S·기초 B1~B12·기둥 P1~P6·받침", "약 40", "일치 (B12 34본은 도면대로가 맞음, 모델 28본 — 사용자 확인)", "%d" % len(ug), "재료표 열 A~J, 정면도·단면도 점 좌표", "재료표·치수·점 좌표·받침·단면까지 완료 (간섭 제외)", ROOT + r"\검토_운곡교P1"],
["산외교(울산) P1", "T형, 확대기초(말뚝 없음), 코핑 C1~C15·기둥 P1~P5", "34", "전부 일치", "%d" % len(sw), "재료표 열 A~J + 형상 도해 I~XII", "재료표·형상 치수 완료 / 위치·피복·받침 미실시", ROOT + r"\검토_산외교P1"],
["옥성교 P1", "T형, 확대기초, 코핑 C1~C20·기둥 P1~P5·받침 10", "46", "UM 92본 제외 전부 일치", "%d" % len(ok), "재료표에 A~J 열 없음 → 철근상세 글자(L= N= a= b= c= j=) 판독", "재료표·철근상세 완료 / 위치·받침 미실시", ROOT + r"\검토_옥성교P1"],
["Ramp-A교 P1", "T형, 말뚝 포함(P1-1·P1-2·P3-2), 코핑 C1~C16", "28", "전부 일치", "%d" % len(ra), "재료표 2장(코핑·기둥/말뚝) + 철근상세", "재료표·철근상세 완료 / 위치·받침·PLATE 미실시", ROOT + r"\Ramp-A교P1"],
]
for r_ in rows5:
w5.append(r_)
rr = w5.max_row
for c in range(1, 9):
w5.cell(row=rr, column=c).border = B
w5.cell(row=rr, column=c).alignment = TOP
w5.row_dimensions[rr].height = 70
for i, w in enumerate([16, 44, 12, 34, 14, 40, 40, 52], 1):
w5.column_dimensions[get_column_letter(i)].width = w
try:
wb.save(OUT)
print("saved", OUT)
except PermissionError:
print("열려 있어 저장 실패")
tot = sum(len(v) for v in ITEMS.values())
print("items", tot, {b: len(v) for b, v in ITEMS.items()})