# 기술노트 — DWG 판독·Rhino 모델링 작업 환경 함정 ``` 소유자: 심영표 확정일: 2026-10-07 최근확인일: 2026-10-20 재확인 조건: ODA·Rhino·Python 버전이 바뀌거나 Rhino MCP 도구가 갱신될 때 ``` ## DWG → DXF - `dwg2dxf`(LibreDWG, 기존 박스 프로젝트 기준 변환기)는 이 PC에 **없음**. ODA File Converter 27.1.0 사용: `C:\Program Files\ODA\ODAFileConverter 27.1.0\ODAFileConverter.exe ACAD2018 DXF 0 1 "*.DWG"` - 한글 경로도 정상 변환. RAG 원본 보호를 위해 임시 폴더에 복사 후 변환 (`Code/교각모델링/convert_dwg.py`) - 콘솔 출력 한글이 깨지면 `PYTHONIOENCODING=utf-8` 지정 (파일 내용 자체는 정상) ## DXF 판독 - Python 3.12에 ezdxf 1.4.4 설치되어 있음. matplotlib는 2026-10-07에 설치(3.11.2) — 렌더링용 - ezdxf TEXT에는 `char_height` 속성이 없음(MTEXT만). `e.dxf.get("char_height")`는 TEXT에서 예외 발생 → dxftype 분기 - ezdxf 렌더 PNG에서 한글은 □로 표시됨(폰트 미매핑). 문자는 survey JSON으로 읽을 것 ## Rhino MCP (Rhino 8, 헤드리스 slot) - `get_viewport_image`가 **회색 빈 화면만 반환**함(Perspective/Front, 카메라 지정 모두). 시각 확인은 대체 수단 필요 — 동일 방식 재시도 금지, 단면 재투영 수치 검증 또는 사용자 육안 확인으로 대체 - 새 문서 기본 단위 mm, 허용오차 0.001. 스크립트는 `__rhino_doc__` 사용, 보관 파일은 `exec(open(path, encoding="utf-8").read(), {...})`로 실행 - 객체 삭제·이동(Transform deleteOriginal) 후 저장하면 **삭제 객체(undo 기록)가 저장 개수에 포함**됨(34 → 68). 저장 전 `doc.ClearUndoRecords(True)` 실행 - `save_doc`은 직전 파일을 `.3dmbak`으로 남김 → 폴더 `backup/`으로 이동 - 검증은 저장 파일을 **새 slot에 open_doc 후 재실행**하면 파일 실재성과 정확성을 함께 확인할 수 있음 - Rhino slot이 작업 도중 닫힐 수 있다(2026-10-07 aardvark, 원인 미확인). `rhino_closed` 오류가 나면 다른 slot을 쓰거나 spawn_slot으로 새로 띄운다 - **`Brep.CreatePipe(..., fitRail=True, ...)`는 직선-원호-직선 레일에서 빈 결과를 내고, 대량 실행 시 헤드리스 Rhino가 크래시함(2026-10-07)** → `fitRail=False` 사용. 동일 방식 재시도 금지 - 대량 생성 전에 형상 종류별로 1개씩 먼저 시험한다 ## 검증 설계 교훈 - 음성 대조(일부러 객체를 옮겨 FAIL이 나오는지 확인)는 반드시 **위치 이동과 높이 이동을 모두** 넣는다. 2026-10-07 대좌 검사기가 Z 검사만 있어 X 3mm 이동을 놓쳤다 → 간격·절대위치 검사를 추가해 해결 - 피복·내부 검사에서 여러 부재의 `ClosestPoint`를 쓰면 부재 간 내부 접합면까지 거리를 재서 음수 피복이 나온다 → 콘크리트를 `CreateBooleanUnion`으로 합친 외곽면 기준으로 잰다 - 검사 정의는 재료표 정의와 맞춘다(예: 재료표 L에 커플러 길이 포함 여부). 1차 FAIL은 증거 파일로 보존한다 - **`for o in doc.Objects`는 숨긴 객체를 건너뛴다.** 사용자가 Rhino 창에서 객체를 숨기면 검증이 '없음'으로 오판한다 → `ObjectEnumeratorSettings(HiddenObjects=True, LockedObjects=True)`로 센다 (2026-10-07 P1-FT) - `open_doc(clearFirst=True)`는 숨긴 객체를 지우지 않는다 → 다른 파일을 열어 비교할 때는 새 slot을 띄운다 - 여러 조각을 이은 곡선의 `Domain.Mid`는 길이 중간이 아니다(세그먼트 매개변수). 비대칭 철근(갈고리 한쪽)의 위치는 바운딩 상자로 잰다 - `Brep.CreateBooleanUnion`은 입력 순서에 따라 실패할 수 있다 → 순서를 바꿔 재시도한다 - 음성 대조는 마크마다 **수평 위치와 절대 높이**를 모두 변형해 본다. 길이·범위 검사만으로는 평행 이동을 못 잡는다(2026-10-07 B9 +5mm 미검출 → 하단 높이 검사 추가) - MCP 호출은 300초 제한이 있다. 철근 관 1,000개 이상의 `VolumeMassProperties`는 제한을 넘는다 → 철근 관 지문은 바운딩 + 면 수로 잰다 - Rhino slot이 다른 호출 중 응답 불능이 되면 MCP가 자동으로 새 slot을 띄워 **빈 문서에서** 다음 스크립트를 실행할 수 있다 → 파일을 연 직후 객체 수(숨김 포함)를 반드시 확인한다. 실패한 열기가 남긴 객체로 문서가 오염되면 slot을 닫고 새로 띄운다 - 갈고리 방향처럼 추정한 형상은 곡면(하면 원호) 근처에서 콘크리트를 뚫을 수 있다 → 콘크리트 내부 표본 검사로 반드시 확인 - 바운딩 최고점·최저점으로 '높이'를 재면 위·아래로 꺾인 갈고리를 잡는다 → 수평부 중간점을 쓴다 - 사각 부재의 '교축방향'을 긴 변으로 판별하지 않는다(받침 NO.3는 교축직각 변이 더 김) → 방향(축과의 각도)으로 판별한다 (2026-10-08) - 검사 항목 설계 시 높이·크기·각도만 넣고 **수평 위치(평면 중심)**를 빠뜨리기 쉽다 → 음성 대조 전에 수평·높이 각각에 대응하는 검사가 있는지 먼저 확인한다 - 세션이 바뀌면(익일) Rhino slot이 모두 종료되어 있다 → 저장 파일을 새 slot에 열고 객체 수부터 확인 ## 보고서 PDF (reportlab) - 2026-10-08 reportlab 5.0.1·pypdf·pypdfium2 설치. 한글은 `C:\Windows\Fonts\malgun.ttf`/`malgunbd.ttf` - **맑은 고딕에는 음수 기호 U+2212(−) 글리프가 없어 빈칸으로 출력** → 변환 시 '-'로 치환. 다른 기호는 fontTools cmap으로 사전 점검 - 표의 짧은 열이 글자 단위로 줄바꿈됨 → 열 최소 폭 = 가장 긴 단어 폭 - 결과 확인은 pypdfium2로 페이지를 PNG 렌더링해 육안 점검 + pypdf 텍스트 추출 ## Rhino 화면 이미지 만들기 (2026-10-14, 발표자료용) - **뷰포트 캡처는 쓰지 말 것**: 이 PC에서 `/runscript`로 띄운 Rhino는 Shaded 모드를 지정해도 `ViewCapture`·`CaptureToBitmap` 결과가 항상 와이어프레임(면 채움 없음). 동일 방식 재시도 금지 - **Rhino 렌더러는 됨**: 스크립트에서 `RunScript("_-Render")` → `_-SaveRenderWindowAs ".png"` → `_-CloseRenderWindow`. 해상도·배경은 `doc.RenderSettings`(`UseViewportSize=False`, `ImageSize`, `BackgroundStyle.SolidColor`, `BackgroundColorTop`). 이미지 1장 약 30초 - 앱 .3dm은 `File3dm.Read`로 읽어 새 문서에 `Objects.Add`로 복사(활성 문서 새로 열린 상태, `OpenHeadless` 아님). 레이어 색은 객체 색으로 복사, 재질을 객체별로 만들어 `Transparency`(0~1)로 콘크리트 반투명 + 철근 색 유지 - **콘크리트(Brep)는 Mesh로 변환해 추가**해야 렌더에 나옴(`Mesh.CreateFromBrep(QualityRenderMesh)` → 합쳐서 추가). 철근 파이프 Brep은 그대로 됨 - 카메라: `vp.SetCameraLocations(target, camera)` — **인자 순서가 (목표점, 카메라점)**(거꾸로 넣으면 아래에서 올려다봄) 후 `ZoomBoundingBox` - 간섭 확대 이미지: 철근 마크별 색 지정 + 나머지 회색, 포커스 점 ± pad 상자로 맞춤. 스크립트 `scratchpad/shot.py`(세션 임시 — 필요하면 `tools/`로 옮길 것) ## 앱 시험 일괄 실행·셸 함정 (2026-10-16) - **시험 일괄 실행기**: `Code/교각모델링앱/tools/run_all_tests.py [시험명...]` — 시험마다 로그 `검증/_rerun_<이름>_<날짜>.log`, 요약 `검증/_rerun_summary_<그룹>_<날짜>.json`. fast = Rhino 없는 12개(약 40초), rhino = 실제 Rhino 7개(수십 분). 같은 그룹명으로 이름 일부만 다시 돌리면 요약 파일이 덮어써지므로 그룹명을 바꿔 돌릴 것(예: rhino2) - **Windows 콘솔(cp949)에서 em dash(—) 등 출력 시 UnicodeEncodeError** — 실행기 상단에서 `sys.stdout.reconfigure(encoding="utf-8")`, 하위 시험은 `PYTHONIOENCODING=utf-8`. 동일 방식 재시도 금지 - **Bash 도구에서 큰 python 헤레독(`python - <<'E'`)에 `'''` 삼중 따옴표가 있으면 "unexpected EOF"로 실패** → 스크립트를 파일로 쓰고 실행(패치 스크립트는 어차피 `Code/`에 보관해야 함). 짧은 코드는 가능 - 백그라운드 `nohup ... &` 로 띄운 작업은 하네스가 완료를 추적하지 않음 — 시작한 셸의 종료 알림이 먼저 오므로 완료로 오해하지 말고 출력 파일의 `DONE` 줄로 확인 - 같은 시험을 재실행하면 `검증/*_data` 폴더는 시험이 다시 만든다(실제 `data/` 교각은 건드리지 않음). 단 `test_okseong`·`test_rampa`·`test_saejae`는 실제 `data/<교각>/runs/current`의 게이트 보고서를 읽으므로 웹 일괄 시험이 덮어쓰면 실패 → 엔진 `tools/run_gates.py /runs/current`로 개별 실행해 복원(OPEN #25) - Rhino 렌더(위 절)로 모델 육안 확인이 가능함 — 새 교각 육안 확인(OPEN #3)에 쓸 수 있는데 아직 안 했음 ## 앱 시험 작성 함정 (2026-10-19) - **`web/store.py`는 `DATA`를 import 시점에 읽는다.** 시험에서 `store`(또는 `app`)를 `PIERAPP_DATA` 환경변수를 설정하기 **전에** import하면(헬퍼 함수 안의 지연 import 포함) 이후 `create_pier` 등이 실제 `data/`에 쓴다 — 2026-10-19 `test_anchors` 첫 실행이 `data/앵커시험`·`data/복사시험`을 실제 데이터에 만들었다(삭제함). 시험은 `os.environ["PIERAPP_DATA"] = ...` → `import app, store` 순서를 지키고, `assert store.DATA == TD`로 확인한다 - 시험 간 상태 의존: `test_pier_batch_button`은 `test_web_batch`가 만든 `배치P1`(전 게이트 승인 상태)을 읽으므로 반드시 바로 뒤에 실행(사이에 `test_individual_after_batch`가 끼면 FAIL). `run_all_tests.py`의 RHINO 목록 순서 유지 - 앵커 따라가기 시험 결과(같은 도면 틀 A 45건 중 오답 0, 다른 틀 36쌍 A·B 0)는 `tools/test_anchors.py`에서 재현 가능 — 재조사 불필요 - 화면 확인 방법: `tools/make_ui_check_data.py`로 임시 데이터를 만들고 `tools/serve_ui_check.py`(포트 5057)를 띄운 뒤 헤드리스 Edge `--screenshot`으로 캡처(`검증/ui_check_shots/`). 서버는 시험 뒤 종료(PowerShell `Get-CimInstance Win32_Process`로 `serve_ui_check` 찾아 `Stop-Process`) - **기하(선 길이) 앵커 시도(2026-10-19) — 동일 방식 재시도 금지**: 앵커 없는 숫자 필드에 콘크리트·철근·중심선 레이어 선분 길이를 대응시켜 새 도면에서 같은 위치·방향 선분을 따라가는 방식은 값이 바뀐 필드를 하나도 복원하지 못했고(울산 P1↔P2, 운곡교 P1↔P2), 철근 위치 목록(좌표)을 길이로 오인해 오답 A가 섞였다. 위치·외곽 점열은 뷰 기준점 대비 좌표라 뷰 기준점을 도면 틀마다 확정해야 한다. 문자 앵커 + 표 교차 확인이 현재 최선 - 운곡교 P3·P4 `pier.json`의 template은 `운곡교p2`(P2를 복사해 만든 교각)다 — 그래서 현재 값 = P2 값, 도면 = 자기 것. 앵커는 `운곡교P1형`을 불러왔고 값은 반영하지 않았다 ## 도면 읽기 도구(`engine/geomread.py`)의 규칙과 함정 (2026-10-20) - **틀**: 운곡교 P1 도면 틀(P1~P4)만. 첫 도면(D1) 좌표가 P1 도면과 같음: 코핑 평면 중심 (12162.4, 26412.3), 정면도 중심 x 12159.4, 단면도 기초 중심 x 30133.6, 기초 하면 y 8115.6 = 기초 EL, 코핑 EL = y + (최고 단 옆 'EL.' 글자 − 그 단 y). 틀 일치율은 템플릿 앵커의 랜드마크 글자 비율(70 % 미만이면 거절). 새재1교·옥성교는 0 %. - **재현 검증이 먼저**: 읽기 규칙은 완성된 교각(P1)에서 같은 값이 나오는지 확인한 뒤에만 믿음 — `tools/test_geomread.py`(P1·P3·P2·P4, 초기 P4 백업 144~150건 = 검증값). - **비교 허용오차는 값의 출처에 따라**: 글자·재료표에서 온 값(기초·말뚝·기둥·철근 개수·길이·반경·재료표 셀·코핑 상단 철근 a)은 **정확 비교**, CAD 선·원·폴리라인에서 잰 값만 1.5 mm(각도 0.05). **동일 방식 재시도 금지**: 모든 숫자에 1.5 mm 를 주면 개수 36 대 37, 재료표 17.247 대 17.377, 상단 철근 a 11855 대 11856 이 '같다'로 가려진다(P2 첫 반영에서 재료표 사본·후프 개수가 빠졌고 P4 G4 C1·C4 2 mm 실패의 원인). - **C14 점은 템플릿 점 좌표에 맞춰 찾지 않는다**: x 오프셋이 교각마다 다름(P1·P3·P4: 13/913…, P2 상단 두 줄: 0/900…). 도면의 철근 원(R27, CS-RBAR-MAJR)을 읽고, 템플릿 C14 높이(+ 높이 이동) 5 mm 안의 원만 취함(기둥 후프 점은 x≈±902 이고 한 점은 C14 높이에서 23 mm 떨어짐, P2 의 C14 x=900 과는 2 mm). 오른쪽 반만 그려져 있어 좌우 대칭, 가운데(|x|≤20)는 한 번, 템플릿에서 왼쪽 짝이 없던 점(P1 x 2913 근처)은 오른쪽 전용. - **재료표의 직경 칸 '"' 는 표 순서상 바로 윗 줄**이다(마크 번호 순서가 아님) — P1~P6 을 번호 순으로 이어받으면 틀림. - **재료표 B9 개수는 격자(체스판)로 계산**: 38×29/2 = 551 = 모델이 놓는 개수, 재료표 493 과 다르면 계획된 FAIL 로 등록(P2·옛 P2). P3·P4 는 격자와 재료표가 같음. - **작도 한쪽만 그려진 값(C14, 기초 중간 층 B11·B12, 좌측 스터럽 윗면)은 좌우 대칭 가정** — 재료표 개수와 어긋나면(P4 B11 30 대 32) 작도를 채택하고 계획된 FAIL 등록 후 설계자 확인. - **`hooks_up` 처럼 점 좌표로 대상을 지정하는 목록은 점이 바뀌면 조용히 깨진다**(P3 G4 코핑 밖 점 56개) — 앱 확인 C9 가 `hooks_up ⊂ c14.points` 를 검사함. - **교각 이름을 바꾸면 모델 객체 이름 접두어(`P1-FT`)가 바뀐다** — `params.display.hidden_ids` 도 같이 바꿔야 함(안 바꾸면 일괄 실행이 마지막 단계에서 오류로 중단). - **고정(F) 항목(pier_id·display)을 도면에서 읽기로 바꾸면 앱 확인 C7 이 사유 기록을 요구** — 반영 화면이 `edit_log.json` 에 자동 기록하도록 수정함(이전 서버 프로세스는 재시작해야 반영). - **실제 사용 데이터에 의존하는 시험은 깨진다**: 사용자가 `운곡교p2`·`운곡교P4` 를 삭제·재작성하면서 시험 6종이 깨짐 → 시험은 G5 까지 승인된 복사본(`testlib.verified_pier`)·휴지통 복사본(`testlib.pier_dir`)을 쓴다. Windows 의 글로빙은 대소문자를 구분하지 않아 `운곡교p2_*` 가 `운곡교P2_*` 를 집는다 — 이름 접두어를 대소문자 구분으로 다시 거른다. - **Flask 는 디버그 꺼짐이면 템플릿을 캐시한다** — 화면·라우트를 고친 뒤에는 사용자가 앱을 껐다 켜야 새 메뉴·버튼이 보인다(2026-10-20 '도면에서 읽기가 없다'의 원인).