dwgrev — 규칙·함정 (불변)
도면이력·겹쳐보기를 손댈 때 먼저 읽는다. 전부 실측·조사로 잡은 것들이다.
🆕 신규
- [코디] poinimos02(두번째 사무실 업무기)에도 「2016 버전으로 변환」 배포 완료(2026-09-09(수) KST, 페페 지시). 원격 SSH 만으로 끝났다 — 페페 조작 0. 설치 전 실태 = ODA 미설치 · 스크립트 없음 · 메뉴 미등록(3개 다 없었다), Python 3.12 만 있었다. 절차 = ⓐ
scp dwg_convert.py register_menu.py 사용설명서 → C:\dev\ⓑmsiexec /i C:\Windows\Temp\oda271.msi /qn /norestart(무창 설치, EXIT=0 →C:\Program Files\ODA\ODAFileConverter 27.1.0\) ⓒpython C:\dev\register_menu.py --pythonw C:\Users\delix\AppData\Local\Programs\Python\Python312\pythonw.exe --script C:\dev\dwg_convert.py. 검증 = 실도면(AC1032)--test→AC1027PASS. 뒤처리 = MSI·로그·테스트도면 삭제, ODA 프로세스 0. - ⚠poinimos02 는 계정명(
poinimos02)과 프로필 폴더(C:\Users\delix)가 다르다 — Python 이 per-user 설치라 등록 명령의 pythonw 경로가C:\Users\delix\...다. 다른 윈도우 계정으로 로그인하면 그 경로가 없어 메뉴가 조용히 죽는다(지금은 대화형 사용자 = 같은 프로필이라 무관). - ⚠업무 중인 PC 라 explorer 재시작은 안 했다(사용설명서 ③단계).
HKCR\*\shell\<verb>는 메뉴를 그릴 때마다 레지스트리를 다시 읽는 정적 verb 라 재시작 없이 뜬다. 안 뜨면 그때만 재로그인. -
⚠Git Bash 에서
scp·msiexec /qn은MSYS_NO_PATHCONV=1없이 쓰면C:/dev/...·/qn이 경로로 변환된다(위 accoreconsole 항목과 같은 함정). -
📏[코디] 크롭 실측(금호 구미 실물, 2026-08-31) — 신
GHgh1A-KF10M= XCLIP 20개·잘라낸 프리미티브 102,971(233,460 → 130,489) · 구GHgh1A-KF08M= 17개·76,577(229,989 → 153,412). 전체의 약 44%가 화면에 없는데 딸려오던 것이었다. 화면 확인 = 표 셀을 넘쳐 뭉개지던 평면도가 각 행 안에 딱 들어간다(dwgrev/_shot_compare.py로 크롭 전/후 세션을 같은 화면으로 찍어 대조). 실물 도면은tools/nasget.py로 NAS 에서 직접 집어온다. - 🔴[코디] XCLIP(크롭)을 무시하면 화면에 없는 전체 평면도가 통째로 딸려 온다(2026-08-31 페페 「크롭한 부분이 적용 안 되고 전체 평면도를 다 가져온다」). 거래처는 큰 평면도를 블록으로 넣고 필요한 부분만 XCLIP 으로 잘라 쓴다.
GeomDump에 클립 처리가 한 줄도 없었다(설계 문서엔 「XCLIP/뷰포트 클립 안쪽」이 게이트 조건으로 적혀 있었는데 구현이 빠져 있었다 — 문서에 있다고 구현된 게 아니다). 처방 =Clip.cs신설 +Add(Ctx,Prim)한 곳에서만 적용(모든 프리미티브가 그 문을 지난다 —Emit마다 걸면 새 타입 추가할 때 빠뜨린다). 중첩 블록은WalkBlock진입에서 push, 빠져나올 때 pop. - 🔑[코디] SpatialFilter 의 두 멤버는 사는 곳이 서로 반대다(2026-08-31 리플렉션 실측) —
Enabled는sf.Definition(=SpatialFilterDefinition)에,OriginalInverseBlockTransform은sf(=SpatialFilter)에 있다. 반대로 쓰면 컴파일이 안 된다. 그리고SpatialFilterDefinition은 값 타입(struct) 이라== null검사가 컴파일 에러다. 좌표계 = 경계점은 XCLIP 정의 시점의 ECS에 있고parentToWcs × (br.BlockTransform × sf.OriginalInverseBlockTransform) × ECS로 WCS 가 된다(정의 후 블록이 안 움직였으면 가운데 곱은 항등). ⚠사각형 클립은 점을 2개(대각)만 저장한다 — 4모서리로 펴야 한다. 경계를 못 읽거나 면적이 0 이면 안 자른다(잘못 자르는 것보다 낫다). - ⚠[코디] COM
SendCommand로-XCLIP을 걸려 하지 마라(2026-08-31 실측, 30초 재시도해도 계속 「피호출자가 호출을 거부」).Visible=False인 AutoCAD 는 프롬프트를 문 채 굳고 COM 이 통째로 막힌다(시드의doc.Export영구정지와 같은 뿌리 — 포커스가 필요하다). 게다가(entlast)는 마지막에 만든 엔티티라 블록 참조 뒤에 선을 하나라도 그리면 엉뚱한 걸 잡아 「객체 선택」에서 멈춘다. ✅처방 = accoreconsole + .NET(_test/ClipGen) — GUI 가 없어 창도 안 뜨고(기본규칙)new SpatialFilterDefinition(pts, normal, elev, front, back, enabled)로 클립을 직접 만든다. 정답을 아는 합성 도면이라 회귀 테스트로 남는다(_test/test_clip.py12항목 = 클립 ON 18선 / OFF 42선). -
⚠[코디] accoreconsole 출력은 UTF-16LE 다 — cp949 로 읽으면 글자 사이에 공백이 낀 것처럼 보인다(시드에 이미 있던 함정을 또 밟았다). 러너(
_test/run_accore.py)가 앞 200바이트의0x00개수로 자동 판별하게 해뒀다.schtasks도 마찬가지로MSYS_NO_PATHCONV=1이 없으면/create가C:/Program Files/Git/create로 변환된다. -
🔴🔴[코디] 구/신은 각자 자기 org(=자기 bbox 좌하단)를 뺀 덤프 좌표다 —
draw()가 그걸 무시하고 겹쳐 그리고 있었다(2026-08-31, 금호 구미 실측 Y로 44,945mm 어긋남). 지금까지 안 터진 이유는 구/신 org 가 우연히 같았기 때문(같은 계열 파일이면 bbox 좌하단이 같다). 담당자가 도면을 통째로 옮겨 보내는 순간 45m 어긋나 나오고,primKey도 절대좌표로 접으므로 지문이 전부 어긋나 「차이 = 100%」가 된다(A/B 실측 = 보정 0.706 vs 상쇄 1.000). 처방 =shiftOf(d)한 곳 —d.org − orgOf()+ 구도면이면 수동 정렬 델타.draw()의SX/SY·격자 컬링 범위(덤프 좌표라sh를 빼서 되돌린다)·primKey·buildDiff의 차이 원 좌표 네 곳이 같은 값을 써야 한다. ⚠「지금까지 잘 됐다」는 org 가 같았다는 뜻일 뿐 보정이 있었다는 뜻이 아니다 — 새 좌표계 코드를 넣을 땐 org 가 다른 세션으로 검증해라(금호 세션 =20260831_111030). - 🔑[코디] 지문의 꼬리 두 값은 대표점 기준 「상대값」으로 접는다(2026-08-31).
primKey가p[len-2], p[len-1]을 절대좌표로 접고 있었는데, ⓐ정렬 델타가 바뀔 때마다 지문이 통째로 달라지고 ⓑ CIRCLE 의 꼬리는 (y, 반지름) 이라 x·y shift 를 더하면 애초에 틀린다.q(p[len-2] − p[4])로 접으면 shift 에 완전 불변이고 모양 정보는 그대로다. - ⚠[코디] 월드 좌표가 4e10 스케일이라 「정확히 같은 점인가」를
1e-6으로 재면 멀쩡한 것도 FAIL 난다(2026-08-31 E2E 에서 밟음). double 의 ulp at 4e10 ≈ 9e-6mm 라base를 더했다 빼는 왕복만으로 그만큼 남는다. 좌표 일치 판정은 1µm(1e-3mm) 로 한다 — 도면 정점 간격 대비 충분히 빡빡하다. -
⚠[코디] 스냅 E2E 에서 「내가 노린 정점에 붙었나」를 기준으로 쓰지 마라(2026-08-31). 실무 도면은 정점이 빽빽해서 노린 정점보다 3px 가까운 다른 정점이 흔하다(실측). 스냅은 정상인데 테스트만 빨개진다. 판정 = ⓐ커서 자리와 다르다(=붙었다) ⓑ실제로 존재하는 정점이다(격자에서 역조회) ⓒ스냅 반경 안. 그리고
page.mouse.click은 정수 픽셀로 반올림된다 — 기대값을 소수 좌표로 계산해 두면 다른 정점에 붙어 오탐이 난다. 클릭 좌표는 파이썬에서round()해서 양쪽에 같은 정수를 쓴다. -
[코디] DWG 우클릭 「2016 버전으로 변환」 = ODA File Converter 래퍼 — 2026-08-31 양 PC 업그레이드(페페 「버전 문제로 변환 안 된다」). 고장 원인은 PC 마다 달랐다: 사무실PC = 레지스트리 명령이
python "C:\dev\dwg_convert.py"인데 그 파일이 없었다(ODA 는 이미 27.1 최신이었다 = ODA 업그레이드로는 안 고쳐졌을 건). 미니PC = ODA 자체 미설치 + 메뉴 미등록. 즉 「버전 문제」로 보였지만 실제로는 설치 누락. - 새 구현 =
C:\dev\tools\local tools\dwg_convert\dwg_convert.py(stdlib 만, 배포 경로 = 양 PCC:\dev\dwg_convert.py) +register_menu.py. 옛 PyInstallerdwg_convert.exe(8.9MB, 소스 유실)는 폐기 — 재빌드 대신 40줄로 다시 썼다. - ⚠ODA 에
ACAD2016출력 버전은 없다 — AutoCAD 2016 은 2013 포맷(AC1027)을 읽는다 → 출력은ACAD2013. CLI =ODAFileConverter <입력폴더> <출력폴더> ACAD2013 DWG 0 1 *.DWG(파일 1개를 임시 in/out 폴더에 넣어 돌린다). - ⚠
reg add로%1을 넣으면 배치 파라미터로 먹혀""가 된다(el.py 로 등록하다 밟음) → 메뉴 등록은 winreg(register_menu.py)로 한다. - ODA 27.1 = 2026-08 기준 최신(opendesign.com 확인).
TD_DbCore문자열 실측 = 인식 시그니처AC1032(2018)까지, 출력 목록ACAD2000~ACAD2018. AutoCAD 2024 는 새 DWG 포맷을 만들지 않았다(웹 확인) → 2018~2026 캐드가 쓴 파일은 27.1 이 커버한다. 아래 73번 「구버전 ODA 가 새 포맷 미지원」은 그대로 유효하고, 최신판으로 올린 지금은 실측 통과다. - 검증 =
python dwg_convert.py --test <파일>(원본 무변경, 결과가AC1027인지 assert) — 미니·사무실 양쪽AC1032 → AC1027PASS + 미니는 덮어쓰기 경로 E2E 까지 PASS. 실패하면 오류창에 원본 캐드 버전을 찍는다(「안 된다」를 진단 가능하게). - ⚠변환 성공했을 때만 원본을 덮어쓴다(실패 시 원본 보존). Win11 미니PC 는 레거시 메뉴라 우클릭 [추가 옵션 표시] 안에 있고, 사무실PC(Win10 19045)는 1단에 바로 뜬다.
- 🔴[코디] 겹쳐보기(DWGCV)의 실체는 캐드 DLL 이 아니라
C:\dev\dwgrev뷰어 서버다 — pcadtools installer 에 그게 안 들어 있다(2026-08-31 사무실PC 실측, 페페 「겹쳐보기 기반이 설치가 안 돼 있다」).DwgCompare.dll만 깔면 덤프는 정상으로 돌고 화면만 안 뜬다 — 사무실PC%LOCALAPPDATA%\PoinDwgViewer\sessions에 그날 누른 세션 2개(8.4MB·7.4MB)가 멀쩡히 쌓여 있었다. 증상이 「아무 일도 안 일어난다」라 DLL·방화벽을 파게 되니 덤프 폴더부터 봐라(있으면 캐드 쪽은 무죄). 새 PC 설치 = DLL 4개 +C:\dev\dwgrev\{server.py,viewer.html}+C:\dev\.secrets\pfs_agent.env(토큰, zip 에 절대 안 넣는다) 3종 세트. 검증 =PYTHONUTF8=1 python C:\dev\dwgrev\server.py --selftest(18항목, Python 3.9 에서도 통과 — 3.7+ 면 된다). -
⚠[코디] SSH(Session 0)로 띄운
pythonw뷰어 서버는 세션이 끊기면 같이 죽는다(2026-08-31 실측 — 띄운 직후 8801 HTTP 200 인데 다음 SSH 호출에선 연결거부·프로세스 0개). 원격에서 미리 상주시켜 둘 수 없다는 뜻이고, 그럴 필요도 없다 —DumpCommands.OpenViewer가 포트가 죽어 있으면 acad 가 직접 띄운다(Session 1). 원격 검증은--selftest로 끝낸다. -
[코디] 뷰어 캔버스는 전부 투명이다 — 바탕색은 CSS
--bg한 곳이 전부다(2026-08-29 검은바탕 토글).draw()가clearRect로 지우므로 캔버스에 칠해진 바탕이 없고, 보이는 흰색은html,body,#stage의background: var(--bg)다. 그래서 테마 전환 =body.dark클래스로 CSS 변수만 갈고scheduleRender(0)이면 끝난다(선 색은render()가 매번getComputedStyle(document.body)로 읽는다 — 하드코딩 색을 넣지 마라, 넣으면 테마가 안 따라온다). 검은 바탕 =--bg:#0a140e(마젠타 보색인 초록을 아주 옅게 깐 검정) · 구--old:#ff00ff(순마젠타) · 신--new:#989898(겹쳐보기) /--new-solo:#ffffff(신도면 단독). ⚠노랑(#ffd21e)은 쓰지 마라 — 검은 바탕에서 흰 선에 묻혀 안 보인다(페페 실사용 즉시 지적). 🔑겹쳐보기에서 신도면은 순백이 아니라 회색#808080이다 — 색이 아니라 휘도 문제였다(순백 1.000 vs 마젠타 0.285 = 3.5배). 화면 잉크의 89%가 최대 밝기면 무슨 색을 줘도 구도면이 묻힌다. 대비를 올리려 하지 말고 신도면을 회색으로 물려라(순백 →#989898). 🔴강조색을 「휘도」로 올리려 들지 마라 — 마젠타는 적·청이 이미 255라 초록을 섞을 수밖에 없고, 그러면#ff70ff처럼 연분홍(파스텔) 이 된다. 휘도 숫자는 0.285→0.401 로 오르는데 눈에는 흐려진다(2026-08-29 페페 교정, 실측 확인). 🔴글로우(drop-shadow)로 강조하지 마라 — halo 가 글자·가는 선까지 흐리게 만든다(2026-08-29 페페, 1턴 만에 반려). 도면 뷰어에서 선명도 > 강조다. ✅강조는 선이 아니라 바탕으로 준다 — 강조색의 보색을 바탕에 아주 옅게(마젠타↔초록,#0f0f0f→#0a140e). 동시대비로 강조색만 튀고 무채색 선은 한 픽셀도 안 변한다(흐려질 여지 자체가 없다). 휘도는 검정과 같게 유지(0.0048→0.0060). ⚠CSS filter 는 캔버스getImageData에 안 남는다(컴포지터 합성) → 그런 효과의 검증은 화면 스크린샷 픽셀로. 「더 밝은 구도면 색」은 헛수고다(가장 밝은 분홍도 0.343 으로 흰색 1.0 에 밀린다). 단 신도면이 혼자 보이면(iso==="new" || !oldOn)--new-solo= 순백 — 그땐 주인공이다. ⚠--fg·--panel은 건드리지 마라 — 조작판이 밝은 크림색--panel위에--fg글자라,--fg를 밝게 바꾸면 조작판 글씨가 안 읽힌다. -
🔑[코디] 차이 찾기는 매칭이 아니라 「지문 개수 빼기」로 한다(2026-08-28 페페 피드백 ①).
DiffEngine.Run()3단 매칭은 여전히 안 쓴다 — 필요한 건 「어디를 봐야 하나」뿐이다. 프리미티브마다 위치(0.5mm 양자화)·모양·글자를 32비트로 접어(primKey) 구 +1 / 신 −1 로 세고, 0 이 아닌 것 = 한쪽에만 있는 것. 실측 = 421k×2 를 450ms. ⚠글자는 문자열 사전 인덱스로 접으면 안 된다 — 사전이 파일마다 달라 같은 글자가 다른 인덱스다. 푼 문자열을 접어야 치수값 변경이 잡힌다. 군집화 = 성긴 격자(80×80) + 8방향 flood-fill. 격자를 촘촘히(160) 잡으면 원 117개가 도면을 덮어 「볼 곳 표시」가 아니라 도배가 된다 → 80 +n>=3잡음컷 + 상위 60개로 36곳. 자른 수는 화면에 적는다(조용한 상한 금지). - 🔴[코디] 좌표계가 둘이다 — 월드 좌표와 덤프(원점이동) 좌표를 섞으면 화면이 백지가 된다.
view.tx/ty는 덤프 좌표(p[4]를 그대로 쓴다), 라벨 영역·차이 원은 월드 좌표(org를 더해 저장). 그래서toScreen/toWorld는 org 를 보정하는데,view를 직접 세팅하는 코드는 보정을 잊기 쉽다 — 차이 원 클릭 확대에서 실제로 물렸다(중심이 도면 밖 25만mm 로 날아가 백지).view를 손으로 세팅할 땐 항상orgOf()를 빼라. - [코디] 팬·줌 버벅임의 처방은 같다 — 「마지막 래스터 시점(raster) → 현재 view」 변환을 CSS 로 붙인다. 줌만 디바운스로 두면 손을 멈출 때까지 화면이 안 변해 버벅여 보인다(페페 피드백 ③).
k = view.s/raster.s·translate((raster.tx−view.tx)·s, H(1−k)−(raster.ty−view.ty)·s) scale(k)·transform-origin: 0 0. 팬은 k=1 인 같은 식이라 코드가 하나로 합쳐진다. 래스터할 때raster를 새로 찍고 transform 을 비운다. - [코디] 겹쳐보기 색은
mix-blend-mode: multiply로 가른다(페페 피드백 ④). 구=순빨강#ff1414/ 신=진남#10233f로 놓고 위 캔버스(신)에 multiply 를 건다 → 겹친 자리는 어두워지고 한쪽에만 있는 선만 원색으로 남는다. 검정×검정이면 구분이 안 됐다. 겹침 표시 캔버스(cRgn)는 블렌드 밖이라 초록 원이 안 죽는다. - ⚠[코디] 클릭 우선순위 — 차이 원은 「가운데 14px」만 먹는다. 원 안쪽 전체를 누름 판정으로 쓰면 큰 원 하나가 그 안의 라벨 영역을 통째로 삼켜 영영 못 고른다(E2E 에서 잡힘).
- ⚠[코디] 떠 있는 패널을 고정
top으로 두지 마라 — 조작판에 줄을 2개 추가했더니#work가 조작판을 덮어 체크박스를 못 눌렀다(Playwrightintercepts pointer events). 열 때마다panel.offsetTop + offsetHeight로 계산해 붙인다. -
⚠[코디] 영역 upsert 는 같은 키면 덮어쓴다 — E2E 가 「행이 안 늘었다」로 오진한다. 앞 실행이 남긴 영역이 있으면
regions.length가 그대로라 타임아웃. 테스트 시작 전에 대상 현장의 영역을 지우고 시작해라(POST .../region {delete:{…}}). -
🔴🔴[코디] 크롬의 GPU 가속 캔버스는 한 Path 에 서브패스가 수만을 넘으면
stroke()가 조용히 아무것도 안 그린다 (2026-08-28 페페 실사용에서 터짐, 실측 확정). 페페 화면이 완전 백지였는데draw()는 「화면 689,104개 · 626ms」를 찍고 있었다 — JS 는 다 돌고 픽셀만 0이다. 에러도 경고도 없다. - 실측 한계 — 서브패스 1,000개 → 불투명픽셀 14,000 · 10,000개 → 95,200 · 50,000개 이상 → 27,200 으로 고정(더 늘려도 안 늘고, 실도면에서는 아예 0).
- 처방 = 끊어 그린다 —
viewer.htmlSTROKE_BATCH = 5000, 프리미티브 5,000개마다ctx.stroke(); ctx.beginPath();. 부수효과로 전체보기 래스터가 774ms → 194ms 로 빨라졌다(거대 Path 조립 비용이 사라진다). - 🔴진짜 교훈 = 헤드리스 Playwright 는 소프트웨어 래스터라 이 버그를 절대 못 잡는다. 단계 4·5·6 검증(성능 실측·비전 대조·E2E 24/24)이 전부 헤드리스여서 통과했는데 실사용은 백지였다. 캔버스·WebGL 을 쓰는 화면은 반드시
headless=False로 한 번 열어getImageData로 불투명 픽셀을 세라. 「그렸다고 보고한 개수」는 증거가 아니다. - 판정 한 줄 =
const d=ctx.getImageData(0,0,c.width,c.height).data; let n=0; for(let i=3;i<d.length;i+=4) if(d[i]) n++; - ⚠[코디] 뷰어를 고쳤는데 브라우저가 옛 사본을 쓰면 「안 고쳐졌다」로 오진한다 —
server.py가.html/.js/.css에Cache-Control: no-store를 붙인다(기하.gz는 세션마다 이름이 달라 캐시돼도 무해). 이건 [[포트 하이재킹]] 과 증상이 같으니 응답 크기 vs 파일 크기 대조로 먼저 서버부터 가른다. -
⚠[코디] 남의 브라우저 화면을 확인할 땐
PrintWindow+ 원색 그대로 — 크롬 창도PW_RENDERFULLCONTENT로 잡힌다. 회색조·반전을 걸면 「캔버스가 진짜 비었나」를 못 가린다.crop.getcolors()로 고유색 수를 세면 한 방이다(백지 = 고유색 1개). 페페 창을 건드리지 않고 원인을 확정한 방법이 이것이다. -
🔴[코디] DLL 배포처는 4곳이 아니라 5곳이다 — 진짜 런타임 경로를 놓치고 있었다 (2026-08-28 실측). 런처가 실제로 NETLOAD 하는 곳은
%LOCALAPPDATA%\pcadtools\다(레지스트리HKCU\...\R24.3\ACAD-7101\Applications\PcadTools의LOADER).%LOCALAPPDATA%\MDCAD\는 옛 경로로 이제dwgcompare_log.txt만 쓴다. 확인해보니pcadtools\DwgCompare.dll이 65,536바이트·8월 6일자로 3주 묵어 있었다 — 단계 3·4·5 산출물이 통째로 안 들어간 상태였다. 배포 대상 =bin\Release·dwgcompare installer·pcadtools installer·%LOCALAPPDATA%\MDCAD·%LOCALAPPDATA%\pcadtools. 판정 = 5곳 MD5 일치. - 🔴[코디] COM
doc.Export(path,"BMP",selset)로 캐드 화면을 뜨지 마라 (2026-08-28 실측). 빈 선택셋을 줘도 명령행에서 「객체 선택」을 물고 0바이트 파일을 만든 채 영구 정지한다(CPU 0%·Responding=True 라 「느린 것」으로 오진하기 쉽다). PostMessage 로 Enter 를 넣어도 안 깨진다 — 포커스가 필요하다. 처방 = MDI 자식(도면) 창을PrintWindow(PW_RENDERFULLCONTENT)— 뷰포트까지 제대로 잡힌다(시드 [[cody-acad-capture-noninvasive]] 가 맞았고cody_capture.py주석의 「PrintWindow는 뷰포트를 검게 잡는다」가 틀렸다). 창 찾기 =MDIClient자식 중 제목에.dwg가 든 것(「시작」 탭이 같은 자리에 있다). - ⚠[코디]
acad.Quit()은 안 죽는 경우가 있다 — 시작 탭만 남긴 채 살아서DwgCompare.dll을 계속 물고, 다음 배포가Device or resource busy로 실패한다. →GetWindowThreadProcessId(acad.HWND)로 PID 를 잡아 확인사살. - ⚠[코디] accoreconsole 을 파이프(
| tail)에 물리면 CPU 100% 로 헛돈다 — 7분에 431초 CPU 를 태우고 도면도 안 연다(작업세트 61MB). 처방 =subprocess.run(..., stdin=subprocess.DEVNULL, stdout=PIPE). 같은 명령이 16.2초에 끝난다. 출력은 cp949 다. - 🔑[코디] acad.exe 루프백 실측 = 새 명령
DWGCV_NET(2026-08-28).TCP 127.0.0.1:8801 = OK·HTTP 200 · 346 bytes(=/sessions응답 크기와 일치) 2회 재현. 단 이 실측은 dev-17 이 방화벽MDCAD_NetBlock_Acad차단대역에서127.0.0.0/8·사설대역·Tailscale 을 뺀 뒤의 상태다(66 CIDR). 「Windows 가 루프백에 방화벽을 안 건다」의 순수 검증은 아니다. - [코디] 현장 목록·타입/위치 트리는 pfs 를 안 건드리고 얻는다 —
_AGENT_PATHS에 이미/api/orderpaper/material-spec/fields(현장 500건{field_no,name,shipping_date}) 와/spec-read/<field_no>(사양 전체doc.types[].groups.{kitchen,general}.locations[]+doc.parts.{body,door}+doc.cores) 가 등록돼 있다. 라벨 3요소(타입/K·F/위치)가 그대로 이 구조다. - ⚠[코디] 뷰어
pfs.rev은 현장을 고르는 순간 「최신 rev」로 이미 채워진다 — E2E 에서wait_for_function("pfs.rev !== null")로 기다리면 즉시 통과해 옛 rev 를 잡는다(그 다음 「이력이 이 rev 에 붙었나」가 가짜 FAIL). 판정은 rev_id 가 바뀔 때까지 기다려야 한다. - [코디] pfs 테스트 env 의 DB 키 이름은
MYSQL_SERVER/MYSQL_PORT/MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE다(DB_*가 아니다). 틀리면1046 No database selected로 죽는다. 위치 = EC2/home/ubuntu/env/pfs-test.env, DB =127.0.0.1:3307/pfs. -
[코디] 해치는 경계 loop 만 그린다 — 비전 대조에서 나온 유일한 차이다.
GeomDump.EmitHatch가 패턴선이 아니라 경계만 담기 때문이고, 의도된 근사다(패턴까지 그리면 프리미티브가 폭증하고, 변경 감지에는 무관하다). 전체보기에서 타입명 같은 큰 글자가 안 보이는 것도 마찬가지로 규칙(TEXT_MIN_PX = 8)이지 버그가 아니다. -
🔴[코디] 방화벽
MDCAD_NetBlock_Acad차단대역에 127.0.0.1 이 들어 있다 (2026-08-28 실측, dev-17 세션 제보로 확인). pcadtools installer 의 「보안 인터넷 차단」이 만드는 규칙이고 대상은acad.exe인/아웃바운드 둘 다(accoreconsole.exe는 대상 아님 → 헤드리스 검증은 항상 된다). CIDR 31개를 파싱해 포함판정하니127.0.0.1·100.108.234.45(미니PC)·192.168.0.101(NAS) 전부 차단대역,13.209.147.168(pfs) 만 뚫려 있다. ✅해소됨 (2026-08-28) — dev-17 이 페페 승인을 받아 차단대역에서127.0.0.0/8·10/8·172.16/12·192.168/16·100.64/10(Tailscale)을 빼 66 CIDR 로 재생성했고(install.ps1·_netblock.bat양쪽 PC), 그 뒤 내가DWGCV_NET으로 acad.exe → 127.0.0.1:8801 실측 OK(위 🔑 항목). 「Windows 가 루프백에 방화벽을 안 거는가」는 규칙에서 이미 빠져 이제 검증 불가 — 되돌릴 이유도 없다. 진단 = 로그가 「원격 서버에 연결할 수 없습니다」면 방화벽 · 「401」이면_AGENT_PATHS·토큰(dev-17 실측).DumpCommands.OpenViewer가 연결 실패 시 이 규칙명을 로그에 찍는다. - ⚠[코디]
_AGENT_PATHS는startswith매칭이다(dev-17 제보). 하위 경로가 여럿이면 의도보다 넓게 열린다. 내/dwgrev하위에도 삭제 경로가 하나 있다 —POST .../regionbody 가{"delete":{…}}면 라벨 사각형 1건 삭제. 수용 판단: 지워지는 건 좌표↔라벨 사각형뿐이고 다시 드래그하면 복구된다. 이력·사양·op_material_spec_cell로는 삭제 경로가 없다. 나중에 이 아래 진짜 삭제 라우트를 달면 토큰만으로 열리니 주의. -
🔴[코디] 파일 덮어쓰기는 임시파일 →
os.replace(dev-17 실사고).io.open(p,"w")는 여는 즉시 truncate 해서 뒤write()가 인코딩 에러로 죽으면 원본이 0바이트로 남는다(2,797줄 JS 가 그렇게 날아갔다·git 으로 복구). 방아쇠 = 파이썬 문자열의 이모지 서로게이트 표기(🎯→surrogates not allowed) → 이모지는 실문자로 써라. -
🔑[코디] 좌표 정확성은 비전 말고
GeometricExtents로 검증한다 (2026-08-28 확립, 명령DWGCV_VERIFY). AutoCAD 가 삽입점·회전·축척·법선을 이미 적용해 돌려주는 값이라, 내parentToWcs × BlockTransform누적이 틀리면 즉시 어긋난다. 수치로 떨어져 눈보다 확실하고 헤드리스로 돈다. 실측 = 대조 73,178 · 불일치 314(0.43%) · 2,964ms. -
🔴[코디] 그 검증에서 판정 기준을 잘못 잡으면 멀쩡한 좌표가 무더기로 FAIL 난다 — 첫 실행 4,246건 FAIL 이 전부 가짜였다. ① 호(Arc)를 원 전체로 bbox 잡지 마라 — 반지름 큰 얕은 호에서 실제보다 수백 배 커진다(최악 29,008%). 양 끝점 + 구간에 실제로 들어오는 사분점만 센다. ② 판정은 「크기 일치」가 아니라 「중심 어긋남」 — 크기는 텍스트 폭 추정·해치 경계 근사·폴리라인 bulge 직선화 때문에 원래 안 맞는다. 이 검증의 목적은 블록 변환이 맞는가 하나뿐이다. ③ 불일치 표본을 볼 때 위치가 겹치면 변환은 맞은 것 — 크기만 다르면 렌더 근사 문제다.
-
🔴[코디] Windows 는
SO_REUSEADDR이 같은 포트 하이재킹을 허용한다 (2026-08-28 실측, 리눅스와 다르다). 옛pythonw서버가 살아 있는 채로 새 서버를 띄우면 둘 다 바인드되고 요청은 먼저 잡은 쪽(=옛 코드)으로 간다 → 코드를 고쳐도 옛 응답이 계속 나와 "왜 안 고쳐지지"로 몇 번을 헤맨다. 판정 = 응답 크기 vs 파일 크기 대조(8,503 bytes 응답 / 17,190 bytes 파일). 처방 = 서버에already_running()가드 +allow_reuse_address = False, 재기동 전Get-NetTCPConnection -LocalPort <p>로 PID 확인 후 종료. ⚠taskkill /F /PID는 MSYS 에서 인자가 깨진다 → PowerShellStop-Process -Id. - [코디] 뷰어 렌더 3종 세트가 실제로 먹는다(446k 프리미티브 실측) — ①world-grid 컬링(64×64, bbox 를
Float64Array로 사전계산) → 확대 상태에서 382개만 그려 4ms ②팬은 CSS transform(컴포지터가 처리, 놓을 때만 재래스터) ③줌은 디바운스(160ms). 전체보기 1회 래스터는 119ms. 구/신은 캔버스 2장 겹치고 위 캔버스opacity만 만진다(재래스터 없이 슬라이더가 즉각 반응). -
⚠[코디] 합성
WheelEvent로 줌을 테스트하면offsetX/Y가 0 이라 뷰가 도면 밖으로 튄다 — "화면 0개"가 나와도 렌더 버그가 아니다. 검증은view를 직접 세팅하고render()를 부르는 쪽이 확실하다. -
🔴[코디] 「미지원」을 총계로만 세면 화면에 있는데 안 그린 것이 숨는다 (2026-08-28, 30MB 도면 실측). 첫 덤프에서 미지원 28,344가 나왔는데 타입별 집계(
Stats.NoDraw(ent, why))를 심어보니 전부 실버그였다 → 410 으로 98.6% 회복. 게이트를 새로 만들 땐 버린 것을 타입별로 세는 코드를 같이 넣어라 — 안 그러면 "잘 걸렀다"와 "통째로 빠뜨렸다"를 구분할 수 없다. - 🔴[코디]
Entity.Explode()결과물을Emit으로 직행시키지 마라 —Walk로 보낸다. 폭발 결과 안에 중첩 BlockReference 가 섞여 있고, Emit 은 블록을 못 다뤄 15,438건이 조용히 사라졌다. - [코디] 폭발이 안 되는 엔티티 3종 —
Spline(11,423건) ·Ellipse(540) ·Solid(533) 는Explode()가 실패한다. 처방 = Spline·일반 Curve 는StartParam~EndParam등분 샘플링(GetPointAtParameter), Ellipse 는 파라미터 그대로 넘겨 뷰어ellipse()로, Solid 는 정점 직접. ⚠AutoCADSolid정점 순서는 0,1,3,2 — 0,1,2,3 으로 이으면 나비넥타이가 된다. - [코디] 기하 JSON 은 gzip 으로 저장하고 서버가
Content-Encoding: gzip만 붙인다 — 30MB 도면 하나가 생 JSON 33MB, gzip 5.3MB(6.2배). 좌표가 대부분이라 압축이 잘 먹는다. 브라우저가 알아서 푼다. - [코디] JSON 크기를 줄이는 두 수 = ①원점 이동(실무 좌표가 3.98e10 라 숫자 하나가 12자 → bbox 좌하단을 빼고 뷰어가 다시 더한다) ②배열 인코딩(
[kind,layer,color,far,…data]— 키 이름을 프리미티브마다 반복하지 않는다). 좌표는 소수 2자리(0.01mm)면 충분. -
⚠[코디] 엔티티 수 ≠ 프리미티브 수 — 75,894 엔티티가 블록 전개 후 446,315 프리미티브가 된다(블록전개 24,579회·최대깊이 10). Canvas 2D 권장치(10~15만)를 넘으므로 world-grid 컬링과 비트맵 캐시는 선택이 아니라 필수다.
-
[코디] pfs API 응답의 payload 는
data가 아니라message다 —RestResponse는{"code":"0000","message":<payload>}로 뱉는다(common/response/rest_response.py).data로 파싱하면 HTTP 200 인데 전부 빈 값이라 "API 가 안 된다"로 오진한다(2026-08-28 E2E 에서 FAIL 4건이 전부 이 한 가지였다). - [코디] Flask
<int:field_no>는 음수를 매칭하지 않는다 — 테스트 프로브로-999를 쓰면 라우트가 안 잡혀 404 가 나고 「미배포」로 오진한다. 프로브는 실재하지 않는 큰 양수(999999)를 쓴다. DB 레벨 프로브(spec_no=-999)는 음수라도 무관 — 라우팅을 안 거치니까. -
[코디] pfs 운영배포는
C:\dev\pfs_deploy_wt워크트리에서 cherry-pick —deploy-live는 별도 워크트리로 이미 체크아웃돼 있어C:\dev\pfs에서git checkout deploy-live가 안 된다. merge 절대 금지(2026-08-28 실측 분기 = master 에만 279커밋 / deploy-live 에만 371커밋 → merge 하면 미배포 다건이 통째로 운영에 나간다).git cherry-pick <내 커밋>→git push origin deploy-live, 그리고 diff --stat 줄수가 내 커밋과 같은지 확인하고 push 한다. -
🔑pfs DB 작업은 로컬에서 직결하지 마라 —
pfs/scripts/ssm_run.py로 EC2 에서 돌린다(2026-08-28 patch48 적용에서 확립). 로컬 py 파일을 base64 로 EC2(i-03225ba8bc12a929e)에 보내python3로 실행하고 출력을 회수한다. 테스트 DB = EC2 로컬 MySQL 8.4.8127.0.0.1:3307(옛 RDSpoin-dbinstance-test는 삭제됨 — 페페가 비용절감으로 EC2 이전), 운영 = RDS 3306. env =/home/ubuntu/env/pfs-test.env·pfs-live.env. ⚠EC2 시스템 python 은 3.6 —subprocess.run(capture_output=True)가 없다(TypeError).stdout=PIPE로 쓰거나 그냥 쓰지 마라. ⚠pfs/scripts/codi_phase*.py5개가 옛 테스트 RDS 호스트를 기본값으로 물고 있다 →getaddrinfo failed. 재사용 전 갱신. - 🔴Bash 도구 heredoc 안 파이썬 문자열에서
\\n이 실제 개행으로 이중디코드된다(2026-08-28 실측). 원격 실행용 스크립트를 heredoc 으로 생성하면print("\\n=== ...")가 파일에서 줄이 끊겨SyntaxError: EOL while scanning string literal로 죽는다. 기존 함정 [bash-heredoc-backslash-double-decode]의 같은 뿌리 — 원격/생성 스크립트는 heredoc 말고 Write 도구로 만든다. -
[코디] 환경변수 덤프에서
TOKEN도 마스킹하라 — 2026-08-28 EC2 env 탐색 때PASS|PWD|SECRET|KEY만 걸러서PFS_AGENT_TOKEN·PFS_TELEGRAM_TOKEN이 로그에 그대로 찍혔다. 마스킹 패턴에TOKEN추가. -
[코디] AutoCAD 2024·2025·2026 은 같은 DWG 포맷(DWG 2024) — 셋은 변환 없이 서로 열린다. DWG 2018 포맷(
AC1032)은 AutoCAD 2018~2023이고 거기서 세대가 갈린다. 보유 AutoCAD 2024 가 거래처 최신 파일을 그대로 읽는다 → 「거래처가 최신 캐드라 못 읽는다」는 걱정은 덤프 경로에는 해당 없다(플러그인 로드는 여전히 2024까지). 2026-08-28 조사. - [페페] ODA File Converter 는 최신 캐드 파일에서 안 돈다(사무실PC 실무 경험, 그래서 Autodesk 뷰어로 수동 변환 중). 원인 = 구버전 ODA 가 새 DWG 포맷 미지원.
ezdxf최신(1.4.4)의 ODA 연동도 R2018까지만 매핑돼 2024 포맷 문자열이 아예 없다. → ODA 를 새 의존성으로 들이지 마라. - 🔴[페페] 거래처는 옛 도면에서 「보이는 객체만 지우고」 누적 재사용한다 — 안 쓰는 블록정의·레이어가 항상 대량으로 쌓여 있다. dwgcompare 가 「너무 자세해서 독」이 된 진짜 원인이 이것. → 모델공간 BTR 에서 도달한 INSERT 의 BTR 만 재귀한다. 전체 BlockTable 순회 금지 — 이 한 줄이 잔재를 통째로 걸러낸다.
- 🔴[코디] 블록 내부 엔티티는 블록 로컬좌표다 — 「코디가 찍은 좌표가 실제와 다르다」의 원인.
Matrix3d childToWcs = parentToWcs * br.BlockTransform;로 BlockTransform 을 통째로 곱한다. 삽입점·회전·축척을 따로 조립하면 비균일 축척+회전에서 교환법칙이 깨져 틀린다. 중첩은 parent × child 순서. 음수 축척 = 미러(텍스트 방향·해치·호 방향까지 뒤집힘). 비균일 축척 걸린 원/호는 타원이 된다. LWPOLYLINE·HATCH 의 OCS 좌표는 Normal 평면 → MCS → block matrix 순(두 번 적용이 흔한 실수).AttributeReference는 정의의 ATTDEF 를 다시 변환하지 말고 인스턴스 기하를 그대로 쓴다. - [코디] 가시성 게이트에서 제외하면 안 되는 것 — 배경색과 같은 색 · PlotStyle · 레이어 잠금(잠긴 건 보인다) ·
thickness == 0· 도면 Extents 밖. 특히IsPlottable은 출력 전용이라 화면 가시성과 무관하다(내 초안 오류, 코덱스가 교정). 「도면 범위 밖」도 숨김이 아니므로 버리지 말고 「원거리 잔재」로 분리 + 기본 OFF 토글로 처리한다. - [코디] 가시성 게이트 포함 조건 — 모델공간에서 도달 가능 ·
!Erased·Entity.Visible그리고 모든 상위 BlockReference.Visible · 레이어IsOff==false && IsFrozen==false(배치면 VP-Freeze) · 참조 BTR 이IsUnloaded·미해결 XREF 아님(로드된 XREF 는 포함) · XCLIP/뷰포트 클립 안쪽 · 해당 annotation 축척에서 표시 · 그릴 기하 있음. ATTRIB 는AttributeCollection에서 별도 수집(비상수 ATTDEF 를 정의에서 그리면 중복). 동적 블록은 원본이 아니라 각 INSERT 의 실제 BTR(대개 익명*U) 을 따라간다. Proxy·Wipeout·이미지·OLE·언더레이는 버리지 말고unsupportedVisible로 집계한다. - [코디] 브라우저 렌더 예산 — SVG 는 DOM 5,000~10,000 에서 무너진다(팬/줌 시 레이아웃 비용). Canvas 2D = 가시 프리미티브 10만~15만·텍스트 2천~5천. WebGL 은 가시선분 30만 초과 또는 p95 프레임 > 16.7ms 일 때만 전환(선굵기·조인·해치·텍스트 아틀라스를 직접 짜야 한다). 구현 = 구/신 캔버스 2장 겹치고 위 캔버스
opacity만 슬라이더로(재래스터화 불필요) ·Path2D스타일별 배치 · world-grid 로 화면 셀만 draw · 8px 미만 텍스트 숨김 · 축소 시 해치는 외곽선만 · 팬은 CSStransform(엔티티 수 무관 60fps) · 줌 멈추면 150~200ms 디바운스 후 재래스터화 · redraw 는requestAnimationFrame안에서만. - [코디] 자족형 HTML 을
file://로 열면 pfs API 토큰이 평문으로 박힌다 — 교차출처라 CORS 와일드카드까지 필요하다. → 로컬 뷰어 서버(pythonw 8801)를 한 겹 끼운다: 브라우저→localhost 동일출처(CORS 0), localhost→pfs 서버 간(CORS 무관), 토큰은C:\dev\.secrets\.env에서 서버가 읽어 HTML 에 안 들어간다. - ⚠[코디] 테스트 RDS
poin-dbinstance-test.cz62ldnopdup...는 DNS 에서 안 풀린다(2026-08-28 실측,getaddrinfo failed).pfs/scripts/codi_phase*.py5개가 이 호스트를 기본값으로 물고 있어 같이 못 돈다. 운영은C:\dev\tools\rdstunnel터널 127.0.0.1:13306 인데 SSM 유휴로 반쯤 끊기면 TCP 는 붙고 쿼리만 무응답(2013 Lost connection) — 「연결됨」으로 오진하지 말고SELECT VERSION()으로 판정하고 터널을 재기동한다. - [코디] MySQL 예약어
by·at을 컬럼명으로 쓰지 마라 —changed_by·changed_at으로 쓴다(patch48 적용). - [코디] 이력 테이블에는 FK 를 안 건다 — 기존
op_material_spec_cell은fk_cell_spec ON DELETE CASCADE지만 그건 「현재값」이라 사상이 다르다. 이력은 감사로그라 원본 spec 이 지워져도 남아야 한다(= 이 프로젝트의 존재 이유). 조회는 인덱스로 받는다.
재발명 금지 — 이미 있는 것
| 자산 | 위치 | 쓰는 법 |
|---|---|---|
| DWG 사이드로드 리더 | dwgcompare-plugin/DwgReader.cs |
Warmup(지연로딩 함정 회피)·ModelSpaceIds·5000개 청크 트랜잭션 패턴 재사용. 30MB·75,894엔티티 6.3초 |
| 퍼센타일 도면범위 | dwgcompare-plugin/DiffEngine.cs RobustExtent |
public static. db.Extmin/Extmax 금지(헤더 19,788,726 vs 실제 292,985 = 67배 오차 실측) |
| 현장×타입×위치×부위 원장 | pfs op_material_spec_cell |
UNIQUE(spec_no,type_id,group_key,location_id,part_key) + src_value/user_value |
| 자재탭 UI | pfs/web/static/js/orderpaper_material.js (2,797줄) |
현장→타입→주방/일반→위치→셀편집 완성품. 위치마다 drawingRef 필드 있음 |
| DWG 자재수집 | acad-tread-plugin/matcollect/ + 지시문_dwg자재수집.md |
사양표 세트별 타입/위치/y구간. 위치 라벨 영역의 시드로 재사용 |
| 질문리스트 도우미 | devplan/assets/qahelper.js |
토글·패널·localStorage·마크다운 복사 구조 그대로 포팅(대상만 DOM→캔버스 사각형) |
안 쓰는 것
DiffEngine.Run()3단 매칭(핸들/정확일치/근접) — 이번 일은 「같다/다르다 판정」이 아니라 「걸러서 그리기」라 매칭 자체가 불필요.Ent.Build()— 지문(해시+대표점)만 만들고 렌더용 전체 좌표가 없다. 좌표 추출은 신규.