diff --git a/docs/images/sample-test-2026-10-19/01-korean-2after.jpg b/docs/images/sample-test-2026-10-19/01-korean-2after.jpg
new file mode 100644
index 0000000..5021a50
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/01-korean-2after.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/01-korean-3current.jpg b/docs/images/sample-test-2026-10-19/01-korean-3current.jpg
new file mode 100644
index 0000000..0f6d96d
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/01-korean-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/02-model-paper-1before.jpg b/docs/images/sample-test-2026-10-19/02-model-paper-1before.jpg
new file mode 100644
index 0000000..95021b6
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/02-model-paper-1before.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/02-model-paper-2after.jpg b/docs/images/sample-test-2026-10-19/02-model-paper-2after.jpg
new file mode 100644
index 0000000..6d239d2
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/02-model-paper-2after.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/02-model-paper-3current.jpg b/docs/images/sample-test-2026-10-19/02-model-paper-3current.jpg
new file mode 100644
index 0000000..9368da8
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/02-model-paper-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/03-large-hatch-1before.jpg b/docs/images/sample-test-2026-10-19/03-large-hatch-1before.jpg
new file mode 100644
index 0000000..8c81e38
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/03-large-hatch-1before.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/03-large-hatch-2after.jpg b/docs/images/sample-test-2026-10-19/03-large-hatch-2after.jpg
new file mode 100644
index 0000000..605f139
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/03-large-hatch-2after.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/03-large-hatch-3current.jpg b/docs/images/sample-test-2026-10-19/03-large-hatch-3current.jpg
new file mode 100644
index 0000000..f06cf4c
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/03-large-hatch-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/04-slug-text-1before.jpg b/docs/images/sample-test-2026-10-19/04-slug-text-1before.jpg
new file mode 100644
index 0000000..670f5ca
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/04-slug-text-1before.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/04-slug-text-2after.jpg b/docs/images/sample-test-2026-10-19/04-slug-text-2after.jpg
new file mode 100644
index 0000000..52fe39d
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/04-slug-text-2after.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/04-slug-text-3current.jpg b/docs/images/sample-test-2026-10-19/04-slug-text-3current.jpg
new file mode 100644
index 0000000..5bb29c3
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/04-slug-text-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/05-mesh-1before.jpg b/docs/images/sample-test-2026-10-19/05-mesh-1before.jpg
new file mode 100644
index 0000000..397ebaf
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/05-mesh-1before.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/05-mesh-2after.jpg b/docs/images/sample-test-2026-10-19/05-mesh-2after.jpg
new file mode 100644
index 0000000..9fd90bd
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/05-mesh-2after.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/05-mesh-3current.jpg b/docs/images/sample-test-2026-10-19/05-mesh-3current.jpg
new file mode 100644
index 0000000..98c7822
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/05-mesh-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/06-lineweight-1before.jpg b/docs/images/sample-test-2026-10-19/06-lineweight-1before.jpg
new file mode 100644
index 0000000..8753f62
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/06-lineweight-1before.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/06-lineweight-2after.jpg b/docs/images/sample-test-2026-10-19/06-lineweight-2after.jpg
new file mode 100644
index 0000000..8336584
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/06-lineweight-2after.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/06-lineweight-3current.jpg b/docs/images/sample-test-2026-10-19/06-lineweight-3current.jpg
new file mode 100644
index 0000000..f136c3a
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/06-lineweight-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/n1-percent-3current.jpg b/docs/images/sample-test-2026-10-19/n1-percent-3current.jpg
new file mode 100644
index 0000000..3df0765
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/n1-percent-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/n2-cp949-symbol-3current.jpg b/docs/images/sample-test-2026-10-19/n2-cp949-symbol-3current.jpg
new file mode 100644
index 0000000..b86dbb1
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/n2-cp949-symbol-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/n3-fit-crop-3current.jpg b/docs/images/sample-test-2026-10-19/n3-fit-crop-3current.jpg
new file mode 100644
index 0000000..94494c9
Binary files /dev/null and b/docs/images/sample-test-2026-10-19/n3-fit-crop-3current.jpg differ
diff --git a/docs/images/sample-test-2026-10-19/results.json b/docs/images/sample-test-2026-10-19/results.json
new file mode 100644
index 0000000..7bbabc4
--- /dev/null
+++ b/docs/images/sample-test-2026-10-19/results.json
@@ -0,0 +1,414 @@
+{
+ "01-korean": {
+ "1before": {
+ "snap": "95fa6a4",
+ "file": "civil.dwg",
+ "loadMs": 5230,
+ "status": "✓ civil.dwg · 17807 entities",
+ "ok": true,
+ "space": "n/a (no spaces API)",
+ "zoom": {
+ "camX": 600800,
+ "camY": 478560,
+ "width": 7000,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "01-korean-1before.jpg",
+ "errors": []
+ },
+ "2after": {
+ "snap": "be8d52f",
+ "file": "civil.dwg",
+ "loadMs": 5650,
+ "status": "✓ civil.dwg · 17810 entities · Layout",
+ "ok": true,
+ "space": "model 1f",
+ "zoom": {
+ "camX": 598950,
+ "camY": 478462,
+ "width": 90,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "01-korean-2after.jpg",
+ "errors": []
+ },
+ "3current": {
+ "snap": "current",
+ "file": "civil.dwg",
+ "loadMs": 775,
+ "status": "✓ civil.dwg · 17810 entities · Layout",
+ "ok": true,
+ "space": "model 1f",
+ "zoom": {
+ "camX": 598950,
+ "camY": 478462,
+ "width": 90,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "01-korean-3current.jpg",
+ "errors": []
+ }
+ },
+ "02-model-paper": {
+ "1before": {
+ "snap": "95fa6a4",
+ "file": "civil.dwg",
+ "loadMs": 5041,
+ "status": "✓ civil.dwg · 17807 entities",
+ "ok": true,
+ "image": "02-model-paper-1before.jpg",
+ "errors": []
+ },
+ "2after": {
+ "snap": "be8d52f",
+ "file": "civil.dwg",
+ "loadMs": 5012,
+ "status": "✓ civil.dwg · 17810 entities · Layout",
+ "ok": true,
+ "image": "02-model-paper-2after.jpg",
+ "errors": []
+ },
+ "3current": {
+ "snap": "current",
+ "file": "civil.dwg",
+ "loadMs": 797,
+ "status": "✓ civil.dwg · 17810 entities · Layout",
+ "ok": true,
+ "image": "02-model-paper-3current.jpg",
+ "errors": []
+ }
+ },
+ "03-large-hatch": {
+ "3current": {
+ "snap": "current",
+ "file": "dwg_18.3mb.dwg",
+ "loadMs": 3693,
+ "status": "✓ dwg_18.3mb.dwg · 82393 entities · Model",
+ "ok": true,
+ "image": "03-large-hatch-3current.jpg",
+ "errors": []
+ },
+ "1before": {
+ "snap": "7a6edd4",
+ "file": "dwg_18.3mb.dwg",
+ "loadMs": 213769,
+ "status": "✓ dwg_18.3mb.dwg · 82393 entities · Model",
+ "ok": true,
+ "image": "03-large-hatch-1before.jpg",
+ "errors": []
+ },
+ "2after": {
+ "snap": "b047dc4",
+ "file": "dwg_18.3mb.dwg",
+ "loadMs": 3597,
+ "status": "✓ dwg_18.3mb.dwg · 82393 entities · Model",
+ "ok": true,
+ "image": "03-large-hatch-2after.jpg",
+ "errors": []
+ }
+ },
+ "04-slug-text": {
+ "3current": {
+ "snap": "current",
+ "file": "BasicSample.dwg",
+ "loadMs": 529,
+ "status": "✓ BasicSample.dwg · 7309 entities · Model",
+ "ok": true,
+ "space": "model 2",
+ "zoom": {
+ "camX": 2900,
+ "camY": 1900,
+ "width": 1100,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "04-slug-text-3current.jpg",
+ "errors": []
+ },
+ "1before": {
+ "snap": "b047dc4",
+ "file": "BasicSample.dwg",
+ "loadMs": 524,
+ "status": "✓ BasicSample.dwg · 7309 entities · Model",
+ "ok": true,
+ "space": "model 2",
+ "zoom": {
+ "camX": 2900,
+ "camY": 1900,
+ "width": 1100,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "04-slug-text-1before.jpg",
+ "errors": []
+ },
+ "2after": {
+ "snap": "fb8f7ca",
+ "file": "BasicSample.dwg",
+ "loadMs": 531,
+ "status": "✓ BasicSample.dwg · 7309 entities · Model",
+ "ok": true,
+ "space": "model 2",
+ "zoom": {
+ "camX": 2900,
+ "camY": 1900,
+ "width": 1100,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "04-slug-text-2after.jpg",
+ "errors": []
+ }
+ },
+ "05-mesh": {
+ "3current": {
+ "snap": "current",
+ "file": "대산당진 2공구_노면_20260804_143854.dwg",
+ "loadMs": 5300,
+ "status": "✓ 대산당진 2공구_노면_20260804_143854.dwg · 65495 entities · Model",
+ "ok": true,
+ "space": "model 70",
+ "zoom": {
+ "camX": 156542.00000000003,
+ "camY": 479804.00000000006,
+ "width": 160,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "05-mesh-3current.jpg",
+ "errors": []
+ },
+ "1before": {
+ "snap": "6f200ef",
+ "file": "대산당진 2공구_노면_20260804_143854.dwg",
+ "loadMs": 5030,
+ "status": "✓ 대산당진 2공구_노면_20260804_143854.dwg · 65486 entities · Model",
+ "ok": true,
+ "space": "model 70",
+ "zoom": {
+ "camX": 156542.00000000003,
+ "camY": 479804.00000000006,
+ "width": 160,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "05-mesh-1before.jpg",
+ "errors": []
+ },
+ "2after": {
+ "snap": "c67a803",
+ "file": "대산당진 2공구_노면_20260804_143854.dwg",
+ "loadMs": 5279,
+ "status": "✓ 대산당진 2공구_노면_20260804_143854.dwg · 65495 entities · Model",
+ "ok": true,
+ "space": "model 70",
+ "zoom": {
+ "camX": 156542.00000000003,
+ "camY": 479804.00000000006,
+ "width": 160,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "05-mesh-2after.jpg",
+ "errors": []
+ }
+ },
+ "06-lineweight": {
+ "3current": {
+ "snap": "current",
+ "file": "대산당진 2공구_노면_20260804_143854.dwg",
+ "loadMs": 5347,
+ "status": "✓ 대산당진 2공구_노면_20260804_143854.dwg · 65495 entities · Model",
+ "ok": true,
+ "space": "model 70",
+ "zoom": {
+ "camX": 156542.00000000003,
+ "camY": 479804.00000000006,
+ "width": 160,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "06-lineweight-3current.jpg",
+ "errors": []
+ },
+ "1before": {
+ "snap": "eec3382",
+ "file": "대산당진 2공구_노면_20260804_143854.dwg",
+ "loadMs": 5068,
+ "status": "✓ 대산당진 2공구_노면_20260804_143854.dwg · 65495 entities · Model",
+ "ok": true,
+ "space": "model 70",
+ "zoom": {
+ "camX": 156542.00000000003,
+ "camY": 479804.00000000006,
+ "width": 160,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "06-lineweight-1before.jpg",
+ "errors": []
+ },
+ "2after": {
+ "snap": "1f80442",
+ "file": "대산당진 2공구_노면_20260804_143854.dwg",
+ "loadMs": 5220,
+ "status": "✓ 대산당진 2공구_노면_20260804_143854.dwg · 65495 entities · Model",
+ "ok": true,
+ "space": "model 70",
+ "zoom": {
+ "camX": 156542.00000000003,
+ "camY": 479804.00000000006,
+ "width": 160,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "06-lineweight-2after.jpg",
+ "errors": []
+ }
+ },
+ "07-layer-toggle": {
+ "3current": {
+ "snap": "current",
+ "file": "dwg_18.3mb.dwg",
+ "loadMs": 3698,
+ "status": "✓ dwg_18.3mb.dwg · 82393 entities · Model",
+ "ok": true,
+ "toggle": {
+ "layer": "CF-PATT",
+ "hideMs": [
+ 21,
+ 18,
+ 13
+ ],
+ "showMs": [
+ 35,
+ 5,
+ 5
+ ]
+ },
+ "errors": []
+ },
+ "1before": {
+ "snap": "fb8f7ca",
+ "file": "dwg_18.3mb.dwg",
+ "loadMs": 3703,
+ "status": "✓ dwg_18.3mb.dwg · 82393 entities · Model",
+ "ok": true,
+ "toggle": {
+ "layer": "CF-PATT",
+ "hideMs": [
+ 565,
+ 454,
+ 512
+ ],
+ "showMs": [
+ 501,
+ 622,
+ 609
+ ]
+ },
+ "errors": []
+ },
+ "2after": {
+ "snap": "3480945",
+ "file": "dwg_18.3mb.dwg",
+ "loadMs": 3603,
+ "status": "✓ dwg_18.3mb.dwg · 82393 entities · Model",
+ "ok": true,
+ "toggle": {
+ "layer": "CF-PATT",
+ "hideMs": [
+ 18,
+ 17,
+ 13
+ ],
+ "showMs": [
+ 26,
+ 5,
+ 6
+ ]
+ },
+ "errors": []
+ }
+ },
+ "n1-percent": {
+ "3current": {
+ "snap": "current",
+ "file": "C0060203-001-표준단면도(2차로,자연환기300m이상).dwg",
+ "loadMs": 528,
+ "status": "✓ C0060203-001-표준단면도(2차로,자연환기300m이상).dwg · 2068 entities · Model",
+ "ok": true,
+ "space": "model 2",
+ "zoom": {
+ "camX": 12700,
+ "camY": 16740,
+ "width": 2600,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "n1-percent-3current.jpg",
+ "errors": []
+ }
+ },
+ "n2-cp949-symbol": {
+ "3current": {
+ "snap": "current",
+ "file": "civil.dwg",
+ "loadMs": 820,
+ "status": "✓ civil.dwg · 17810 entities · Layout",
+ "ok": true,
+ "space": "model 1f",
+ "zoom": {
+ "camX": 162366.6,
+ "camY": 475780,
+ "width": 9,
+ "up": [
+ 0,
+ 1
+ ]
+ },
+ "image": "n2-cp949-symbol-3current.jpg",
+ "errors": []
+ }
+ },
+ "n3-fit-crop": {
+ "3current": {
+ "snap": "current",
+ "file": "산외교- ELEVATION 산출근거도 (울산방향).dwg",
+ "loadMs": 738,
+ "status": "✓ 산외교- ELEVATION 산출근거도 (울산방향).dwg · 11262 entities · Model",
+ "ok": true,
+ "image": "n3-fit-crop-3current.jpg",
+ "errors": []
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/sample-test-report-2026-10-19.html b/docs/sample-test-report-2026-10-19.html
new file mode 100644
index 0000000..fd02a4b
--- /dev/null
+++ b/docs/sample-test-report-2026-10-19.html
@@ -0,0 +1,1238 @@
+
+
+
+
+
+샘플 테스트 보고서
+
+
+
+테마 전환
+
+샘플 테스트·오류 처리 현황 보고서 (2026-10-19)
+
+대상: dwg-dxf-viewer-sample (HEAD 1f80442 + 미커밋 작업 트리)
+근거: 이번 재테스트 실측, git 이력 20커밋, docs/*.md 10건, hmwebviewer/PROGRESS.md
+상태 표기: 원인 확정 · 수정 완료·검증됨 · 수정 완료·미검증
+이 보고서에는 검증된 결과만 싣는다. 검증하지 못한 항목은 §8 목록에만 적는다.
+
+
+1. 핵심
+
+고유 샘플 20개(DWG 19 + DXF 1)가 모두 브라우저 뷰어에서 로드에 성공했다. 콘솔 에러는 404 리소스 1건뿐이다. Node 헤드리스 파싱도 19/19 성공했다.
+기존 e2e(e2e_lin_test.cjs)는 지금 상태 그대로는 실패한다 (ERR_MODULE_NOT_FOUND). 끝까지 돌려도 assert 가 없어서 결과를 판정하지 못한다.
+과거에 처리한 오류 13건 중 8건을 검증했다 (§4.1). 해당 커밋의 수정 전 버전과 수정 후 버전을 실제로 띄워서, 현재 버전까지 3단으로 캡처하고 측정했다.
+대용량 로드: 213.8초 → 3.6초
+Slug 글자 높이: 150px → 202px (비율 0.743, 커밋 주장 0.744)
+레이어 토글: 454622ms → 1335ms
+그 외 회색 원 제거, 메쉬 표시, 선가중치 적용, Model/Paper 분리, 한글 복원
+
+
+이번에 새로 찾은 문제는 4건이다.
+Fit 이 도면 가장자리를 잘라냄
+%%% 제어코드가 변환되지 않음
+기호만 있는 CP949 문자열이 깨진 채 남음
+e2e 스크립트가 고장 나 있음
+
+
+라이선스 2건 (원인 확정)
+공개한 MPL 패치를 원본 acadrust 0.4.1 에 적용하면 실패한다. 실제 wasm 의 소스를 재현할 수 없다는 뜻이다.
+Visio 저작권("All rights reserved") 라인타입 데이터가 번들에 들어 있다.
+Gitea 와 dist2 는 사내 전용이다. 외부 반출(고객 납품·외부 웹 공개·npm) 전에 해결해야 한다. 라이선스 고지 버튼 자체는 형식이 맞다. 그 버튼이 가리키는 패치 내용이 틀렸다.
+
+
+라이브러리 배포는 조건부로 가능하다 (§6). 위 라이선스 2건, Vite 전용 코드, Viewer2D 에 dispose() 가 없는 문제를 먼저 해결해야 한다.
+
+2. 권장 액션
+
+라이선스.
+MPL 패치를 다시 만든다. 원본 0.4.1 과 실제 vendored 트리의 전체 diff 로 만들고, 원본에 적용해서 성공하는지 확인한다.
+고지 문서 4곳의 "한 파일만 수정", "unmodified" 문구를 고친다.
+ICAD_LIN 은 IntelliCAD(ITC) 계약 범위를 확인한다. 외부 배포가 허용되지 않으면 제거한다.
+
+
+미커밋 작업 정리.
+2.5D 보기, VIEWPORT 채움 변환, Z 붕괴 수정을 커밋하거나 분리한다.
+루트의 untracked 대용량 파일(C0060202.json 31.7MB, SAN01.json 16.6MB, C0060202.zip)은 .gitignore 에 넣거나 삭제한다.
+
+
+e2e 를 실제 테스트로.
+linParser.ts 의 확장자 없는 import 를 고친다.
+샘플 경로를 dist/ 에서 public/samples/ 로 바꾼다.
+assert 를 추가한다.
+이번에 쓴 브라우저 e2e 와 3단 비교 스크립트를 레포에 넣는다.
+
+
+Fit 잘림 수정 (§3.4-①).
+텍스트 후처리 2건 수정.
+stripMText 에 %%%, %%nnn, %%o, %%u 처리를 추가한다.
+fixCp949Mojibake 가 한글이 없는 CP949 기호도 복원하게 한다.
+
+
+
+
+3. 이번 재테스트 결과
+3.1 환경
+
+
+
+항목
+값
+
+
+
+코드
+HEAD 1f80442 + 미커밋 변경(2.5D 보기, _bakeFillXf, _trimmedContentBox Z 수정)
+
+
+Node
+v24.11.1
+
+
+브라우저
+시스템 Chrome headless, playwright-core@1.56.1
+
+
+GPU
+ANGLE D3D11 / NVIDIA RTX 3060 (실제 GPU)
+
+
+타입체크
+npm run typecheck 통과 (컴파일 통과일 뿐, 동작 검증은 아님)
+
+
+3.2 샘플별 결과
+열 설명:
+
+Node 파싱 : parse_dwg_json + fixDwgKoreanText 실행 시간.
+RSS : 프로세스 전체 메모리.
+브라우저 로드 : 파일을 선택한 뒤 상태바에 ✓ <파일명> 이 뜨기까지의 시간.
+JS heap : performance.memory.
+notif : acadrust notification 개수.
+
+
+
+
+파일
+MB
+버전
+엔티티
+Node 파싱 ms
+RSS MB
+브라우저 로드 ms
+JS heap MB
+콘솔 에러
+notif
+결과
+
+
+
+BasicSample.dwg
+0.4
+R2018
+7,309
+142
+147
+553
+47
+1¹
+4
+통과
+
+
+civil.dwg²
+5.4
+R2000
+17,810
+389
+264
+799
+98
+0
+2
+통과
+
+
+C0060203-001-표준단면도(…).dwg
+0.4
+R2007
+2,068
+93
+126
+567
+57
+0
+78
+통과
+
+
+bb.dwg
+0.7
+R2018
+438
+75
+109
+579
+46
+0
+4
+통과
+
+
+simple.dxf
+0.0
+DXF
+6
+—
+—
+74
+34
+0
+—
+통과
+
+
+04manhole1-simplify.dwg
+0.4
+R2018
+7,035
+118
+138
+575
+54
+0
+4
+통과
+
+
+dwg_18.3mb.dwg
+18.4
+R2018
+82,393
+1,988
+1,087
+4,232
+372
+0
+4
+통과
+
+
+test.dwg³
+18.4
+R2018
+82,393
+2,156
+1,088
+3,914
+371
+0
+4
+통과
+
+
+대산당진 2공구_노면_….dwg
+37.1
+R2018
+65,495
+3,017
+1,373
+5,454
+782
+0
+4
+통과
+
+
+a. 거더배치 삽도
+0.2
+R2018
+1,433
+118
+134
+568
+139
+0
+5
+통과
+
+
+a. 교대배치 삽도
+0.4
+R2018
+2,138
+58
+102
+565
+59
+0
+4
+통과
+
+
+a. 교량시종점 삽도
+0.4
+R2018
+4,432
+84
+105
+542
+56
+0
+4
+통과
+
+
+a. 교량폭원 삽도
+0.5
+R2018
+2,824
+64
+106
+575
+45
+0
+4
+통과
+
+
+a. 교면포장 삽도
+0.2
+R2018
+1,304
+54
+104
+570
+44
+0
+4
+통과
+
+
+a. 중앙분리대 삽도
+0.4
+R2018
+2,159
+63
+103
+537
+44
+0
+4
+통과
+
+
+a. 횡단경사 삽도
+0.5
+R2018
+5,635
+126
+138
+569
+44
+0
+4
+통과
+
+
+산외교- ELEVATION 산출근거도
+0.6
+R2007
+11,262
+162
+181
+937
+111
+0
+44
+통과
+
+
+산외교-받침배치도
+0.4
+R2018
+6,240
+106
+144
+635
+63
+0
+4
+통과
+
+
+산외교_파이형_교각일반도(울산P11-꺾임)
+1.0
+R2018
+25,927
+371
+219
+810
+56
+0
+4
+통과
+
+
+산외교_파이형_교각일반도(울산P9)
+1.0
+R2018
+26,207
+397
+220
+744
+57
+0
+4
+통과
+
+
+¹ 404 리소스 1건, 첫 페이지에서만 발생. /favicon.ico 가 404 를 반환하는 것은 확인했다.
+² civil.dwg 와 C0060202-001-평면및종단면도(…).dwg 는 md5 가 같다. 같은 파일이다.
+³ test.dwg 는 dwg_18.3mb.dwg 와 바이트는 다르지만, 파싱 결과의 엔티티 수와 텍스트 통계가 같다.
+"통과" 의 기준: 상태바에 ✓ <그 파일명> · … entities 가 표시되고, .err 가 없고, 페이지 crash 가 없을 것.
+3.3 기존 e2e (e2e_lin_test.cjs)
+
+
+
+실행 방식
+결과
+
+
+
+node e2e_lin_test.cjs (그대로)
+실패, exit 1 . 4단계에서 ERR_MODULE_NOT_FOUND: …/src/viewer2d/linData
+
+
+resolve hook 추가
+끝까지 실행, exit 0. H-DICHL06/06R/02R/01R/05 패턴 5개 출력
+
+
+원인 확정: linParser.ts 가 ./linData 를 확장자 없이 import 한다. Vite 는 이것을 해석하지만 Node ESM 은 해석하지 못한다.
+그 밖의 문제:
+
+assert 가 없다. 패턴을 못 찾아도 console.error 만 찍고, 마지막에 항상 COMPLETED SUCCESSFULLY 를 출력한다.
+샘플 경로가 gitignore 된 ./dist/samples/… 다. clean checkout 에서는 실패한다.
+느린 경로인 parse_dwg 를 쓰고 fixDwgKoreanText 를 적용하지 않는다. 그래서 출력되는 라인타입 설명이 LÇüÃø±¸ 처럼 깨진다.
+Found 0 text styles 가 출력된다. tables.styles 가 비어 있다는 문서상 과제가 C0060202 에서도 재현된다.
+
+3.4 이번에 새로 확인한 문제
+① Fit 이 도면 가장자리를 잘라냄. 재현 확인.
+
+ELEVATION 도면에서 Fit 을 하면 도곽 왼쪽이 화면 밖으로 나가고, 오른쪽에는 약 470px 가 비어 있다 (§4.1 캡처).
+자동 Fit, Fit 버튼, 창 크기 변경 후 Fit 모두 같은 결과다.
+HEAD(미커밋 변경 제외)에서도 똑같다. WIP 회귀가 아니다.
+
+② AutoCAD 제어코드 %%% 등이 변환되지 않음. 원인 확정.
+
+C0060203 에 -2 %%%, civil Layout 에 S=0.0000%%% 가 그대로 표시된다.
+Viewer2D.js:64 의 stripMText 는 %%d, %%p, %%c 만 처리한다.
+
+③ 기호만 있는 CP949 문자열이 깨진 채 남음. 원인 확정.
+
+④ e2e 스크립트 고장. §3.3 참고.
+기타 (경미)
+
+Layout 탭 이름이 Layout (Paper_Space0) 처럼 내부 블록명으로 나온다. dwg2model 삽도 7개 전부가 그렇다.
+Layout 탭이 많으면 툴바가 줄바꿈되면서 상태바가 속성 패널과 겹친다.
+parse_dwg_json 은 acadrust notification 의 개수만 돌려주고 내용은 돌려주지 않는다 (C0060203 78건, ELEVATION 44건).
+
+
+4. 처리된 오류 이력
+표기: [M] 커밋 메시지 · [D] diff 로 확인 · [Doc] 문서 기록.
+Rust 쪽 수정(dwg-wasm, acadrust)은 hmwebviewer/rust/dwg-wasm 에 있다.
+
+
+
+#
+시기
+증상
+재현 샘플
+원인
+조치
+커밋
+이번 검증
+
+
+
+1
+07-30
+구형 한글 TEXT/MTEXT 깨짐
+civil 외
+CP949 바이트가 Latin-1 로 올라옴 [Doc]
+fixDwgKoreanText.ts (뷰어)
+be8d52f
+수정 완료·검증됨 (§4.1-①)
+
+
+2
+07-30
+Model 과 Paper 동시 렌더, Layout 비어 보임, Fit/좌표, 글자 거대, VP 순서 (6건)
+C0060202
+VIEWPORT 미투영, minTextH 등 [Doc]
+cadSpaces.ts, Viewer2D (뷰어)
+be8d52f
+수정 완료·검증됨 (§4.1-②)
+
+
+3
+07-30
+Brave/Firefox 에서 OLE BMP 미표시
+—
+<img> 가 data:image/bmp 미지원 [M]
+순수 JS BMP 디코더 [D]
+963a903
+§8
+
+
+4
+07-30
+OLE2FRAME 이 하드코딩된 좌표에 고정됨
+—
+정답 좌표 하드코딩 [D]
+파서가 준 ul/lr 사용
+76f3e20
+§8
+
+
+5
+08-03
+Slug 텍스트 cap-height 약 26% 작음
+BasicSample
+3d3e052 의 회귀 [D]
+되돌림
+fb8f7ca
+수정 완료·검증됨 (§4.1-④)
+
+
+6
+08-03
+19MB DWG 로딩 시 탭 프리즈
+dwg_18.3mb
+wasm 힙 O(n²) [M]
+parse_dwg_json 스트리밍
+b047dc4
+수정 완료·검증됨 (§4.1-③)
+
+
+7
+08-03
+도면 위아래 거대 회색 원
+dwg_18.3mb
+sample_arc 각도 swap 등 [Doc]
+손상 호 감지 후 생략 (파서)
+b047dc4
+수정 완료·검증됨 (§4.1-③)
+
+
+8
+08-04
+레이어 토글 지연
+dwg_18.3mb
+토글마다 load() 재빌드 [M]
+setDrawRange, .visible 토글
+3480945
+수정 완료·검증됨 (§4.1-⑦)
+
+
+9
+08-04
+노면/부체도로면(MESH) 미표시
+대산당진 노면
+MESH 직렬화 누락, read_mesh 100k 상한 [M]
+acadrust 패치 + 직렬화 + _meshSegs
+eec3382, c67a803
+수정 완료·검증됨 (§4.1-⑤)
+
+
+10
+08-04
+모든 선이 1px
+대산당진 노면
+lineWeight 누락, WebGL linewidth 무시 [M]
+LineSegments2 버킷
+c67a803
+수정 완료·검증됨 (§4.1-⑥)
+
+
+11
+08-04~05
+선가중치 배율 (8 → 20 → 11 px/mm)
+대산당진 노면
+배율 [M]
+기본값 11, 옵션 패널
+cf12987, a924b1e, 1f80442
+굵기 적용 확인 (§4.1-⑥). 배율 일치는 §8
+
+
+12
+08-04
+MPL-2.0 고지 누락
+—
+고지/원문/소스 경로 없음 [M]
+NOTICES, public/licenses/
+ce3859c
+조치 불충분 : 패치가 원본에 적용되지 않음 (§5 A-1)
+
+
+13
+08-04
+vite build 가 dist2 테스트 파일을 삭제
+—
+emptyOutDir 기본값 [M]
+--emptyOutDir false 절차 문서화
+6f200ef
+—
+
+
+분리 전 hmwebviewer 에서 처리한 것 (07-10~14, PROGRESS.md):
+
+solid HATCH 구멍, HIDDEN/CENTER 파선 무시
+MAX_TEXTS 3000 상한
+회전 도곽 viewTwist, 로고 해치, 폴리라인 폭, MTEXT 배경
+
+미커밋 수정 2건 (수정 완료·미검증):
+
+VIEWPORT 안 SOLID/HATCH 채움에 model→paper 변환 추가 (_fillXf/_bakeFillXf)
+_trimmedContentBox 의 Z 붕괴 수정
+
+4.1 수정 전 / 수정 후 / 현재 비교 캡처
+방법
+
+각 버전은 git archive <커밋> 으로 풀어서 실행했다. wasm 이 커밋마다 함께 커밋돼 있어서, 그 시점의 파서와 뷰어가 같이 재현된다.
+계측용으로 두 가지만 덧붙였다. 둘 다 동작에는 영향이 없다.
+main.ts 에 window.__viewer = viewer 1줄
+vite 캐시 경로 분리
+
+
+"현재" 는 오늘의 작업 트리다.
+로드 판정은 상태바가 ✓ <그 파일명> · 으로 바뀌었는지로 했다.
+확대 화면은 모든 버전에서 같은 WCS 좌표와 화면 폭으로 카메라를 고정했다. 파일럿 캡처로 목표 객체가 화면 중앙에 오는 것을 먼저 확인했다.
+UI 패널은 숨기고 캔버스만 찍었다. 해상도 1280×800, RTX 3060.
+24회 실행 모두 로드 성공, 콘솔 에러 0건.
+
+① 한글 깨짐 (civil.dwg)
+수정 전 버전의 wasm 이 실제로 내보낸 문자열과, 수정 후 화면을 함께 비교했다:
+
+
+
+handle
+수정 전 95fa6a4 wasm 출력
+수정 후 be8d52f
+
+
+
+3232403
+Ãø Á¡
+측 점
+
+
+3232405
+°èȹ°í
+계획고
+
+
+3232408
+Áö¹Ý°í
+지반고
+
+
+3240601
+õÀÇ1Áö±¸ °èȹ¼±
+천의1지구 계획선
+
+
+3241973
+(»ç¾÷ ÁøÇà ¿¹Á¤)
+(사업 진행 예정)
+
+
+1009
+°Ç¼³´Ü°è
+건설단계
+
+
+
+
+
+수정 후 be8d52f
+현재
+
+
+
+
+
+
+
+② Model/Paper 겹침, Layout 비어 보임 (civil.dwg, 로드 직후 기본 화면)
+
+
+
+수정 전 95fa6a4
+수정 후 be8d52f
+현재
+
+
+
+
+
+
+
+
+점 3개만 보임. Model(x≈600k·162k)과 Paper(원점)를 한꺼번에 Fit 함
+Layout 이 열리고 VIEWPORT 내용이 투영됨
+같음 (밝은 종이 배경)
+
+
+③ 대용량 로딩 + 거대 회색 원 (dwg_18.3mb.dwg, 로드 직후 기본 화면)
+
+
+
+수정 전 7a6edd4
+수정 후 b047dc4
+현재
+
+
+
+
+
+
+
+
+로드 213.8초 . 회색 SOLID HATCH 가 도면 아래쪽 절반을 덮음
+3.6초 , 회색 채움 없음
+3.7초
+
+
+④ Slug 텍스트 크기 (BasicSample.dwg, "정면도" h=140 주변 고정 화면)
+
+
+
+수정 전 b047dc4
+수정 후 fb8f7ca
+현재
+
+
+
+
+
+
+
+
+글자 높이 150px
+202px
+202px
+
+
+
+자홍색 픽셀 bbox 높이를 쟀다. 비율 150/202 = 0.743 으로, 커밋 메시지 "3.6 → 2.68 (0.744)" 와 일치한다.
+주변 선과 치수의 위치는 세 장 모두 같다.
+
+⑤ SubDMesh(MESH) 미표시 (노면 도면, 연결부 (156542, 479804), 폭 160)
+
+
+
+수정 전 6f200ef
+수정 후 c67a803
+현재
+
+
+
+
+
+
+
+
+삼각망 없음
+도로면·부체도로면 삼각망 표시
+같음
+
+
+c67a803 에는 선가중치 수정도 함께 들어 있다.
+⑥ 선가중치 (⑤와 같은 화면)
+
+
+
+수정 전 eec3382
+수정 후 1f80442 (11 px/mm)
+현재
+
+
+
+
+
+
+
+
+0.50mm 도로마킹 포함 모든 선 1px (메쉬 렌더러 이전 버전)
+굵게 표시
+같음
+
+
+⑦ 레이어 토글 속도 (dwg_18.3mb.dwg, setHiddenLayers(CF-PATT) 숨김→표시 3회, 동기 실행 시간)
+
+
+
+
+수정 전 fb8f7ca
+수정 후 3480945
+현재
+
+
+
+숨김 ms
+565 / 454 / 512
+18 / 17 / 13
+21 / 18 / 13
+
+
+표시 ms
+501 / 622 / 609
+26 / 5 / 6
+35 / 5 / 5
+
+
+새로 확인한 문제 (현재 버전)
+
+
+
+%%% 미변환 (C0060203)
+CP949 기호 잔존 (civil)
+Fit 잘림 (ELEVATION)
+
+
+
+
+
+
+
+
+-2 %%% (정답 -2 %)
+500¡¿350 (정답 500×350)
+왼쪽 잘림, 오른쪽 약 470px 공백
+
+
+
+5. 남은 과제
+A. 우선 처리
+
+
+
+#
+과제
+근거
+
+
+
+A-1
+MPL 패치가 실제 소스를 재현하지 못함 (원인 확정) - public/licenses/acadrust-0.4.1-read_mesh.patch 를 원본 acadrust 0.4.1 crate 에 git apply --check 하면 entities.rs:2350(read_ole2frame)에서 실패한다. - 고지의 "One file differs", "exact Source Code Form" 문구와 acadrust-dwg/package.json 의 "used unmodified" 가 사실과 다르다.
+이번 실행, THIRD-PARTY-NOTICES.md
+
+
+A-2
+Visio 저작권 데이터가 번들됨 - linData.ts 의 ICAD_LIN 헤더가 "Copyright (c) 1998 by Visio Corporation. All rights reserved" 다. - linParser.ts:228 이 import 될 때 등록하므로 배포본에 반드시 들어간다. - IntelliCAD(ITC) 계약이 자사 웹 뷰어로의 외부 배포를 허용하는지 확인이 필요하다.
+이번 확인
+
+
+A-3
+자체 LICENSE 파일 없음, NanumGothic OFL 원문 미동봉
+§6
+
+
+A-4
+자동 판정 테스트 없음, e2e 고장
+§3.3
+
+
+A-5
+Fit 이 가장자리를 잘라냄
+§3.4-①
+
+
+A-6
+미커밋 WIP 정리, src/topProgress.ts 어디서도 import 되지 않음, 루트 대용량 untracked 파일
+git status
+
+
+A-7
+원본 hmwebviewer 에 Model/Paper 수정 미이식 (cadSpaces.ts 없음)
+model-paper-space.md §9
+
+
+B. 기능·품질
+
+
+
+#
+과제
+근거
+
+
+
+B-1
+해치 손상 호의 근본 원인 미확인. 현재 수정은 감지 후 생략. spurious_r 5배 임계값은 경험값
+hatch-boundary-arc-artifact.md §7
+
+
+B-2
+파싱 중 메인 스레드 블로킹. 37MB 도면 로드 5.5초 동안 UI 정지. Web Worker 미적용
+이번 측정
+
+
+B-3
+메쉬 레이어에 굵기가 지정되면 fat 버킷 상한(40만) 124.5% 초과 → 버킷 전체 헤어라인 폴백
+improvement-results §5-3
+
+
+B-4
+%%% 등 제어코드, CP949 기호
+§3.4-②③
+
+
+B-5
+tables.styles 비어 있음 → 폰트 매핑 불가
+한글 문서 §8, 이번 e2e
+
+
+B-6
+한글 처리가 JS 휴리스틱. 932/936/950 미지원
+한글 문서 §8
+
+
+B-7
+Layout 탭 캡션이 내부명
+이번 실행
+
+
+B-8
+acadrust notification 내용 비노출
+이번 실행
+
+
+B-9
+40만 상한 초과 시 사용자 알림 없음 (console.warn 만)
+lineweight-rendering.md §6
+
+
+B-10
+wasm32 4GB 한계. 37MB 도면에서 Node RSS 1.37GB, 브라우저 JS heap 782MB
+이번 측정
+
+
+C. 나중에
+
+선가중치: 중첩 ByBlock 미추적, DXF LAYER 코드 370 무시, celweight 가 DWG 인덱스 그대로 나감
+MESH: Catmull-Clark(subdivisionLevel > 0), POLYFACE/POLYGON MESH, shaded 패스
+VIEWPORT: 솔리드 HATCH 근사, 비직교 VP, TILEMODE/CTAB 휴리스틱
+OLE BMP 가 24/32bpp 만 지원
+parse_dwg export 미정리
+CXGLOGO-EX흑백.dwg 의 얇은 원 (hmwebviewer 시절부터)
+
+D. 배포·문서 정리
+
+build:dist2 스크립트 없음, dist2/assets 에 예전 해시 번들 누적, 테스트 도면이 dist2/samples 에 섞여 있음
+문서 상태 표기가 낡음:
+subdmesh-rendering.md: "렌더러 미커밋"
+lineweight-rendering.md, improvement-results: "미푸시"
+여러 문서: "acadrust 미수정"
+
+
+Viewer2D.d.ts:24, Viewer2D.js:1389 주석은 "Default 20" 인데 실제 기본값은 11
+README 이슈 표에 hatch, large-dwg 문서 누락
+
+
+6. 라이브러리 배포 가능성
+6.1 결론: 조건부 가능
+
+파서는 DOM 없이 동작한다. Node 24 에서 initSync 로 19개 도면을 파싱해 실측했다.
+Viewer2D 가 DOM 에 닿는 곳은 생성자로 받는 container 하나뿐이다.
+다만 아래 차단 요소를 먼저 풀어야 한다.
+
+6.2 차단 요소
+
+
+
+#
+문제
+위치
+영향
+
+
+
+L1
+MPL 패치로 소스 재현 불가
+§5 A-1
+법적 의무 미충족
+
+
+L2
+Visio 저작권 lin 데이터
+linData.ts:2
+재배포 권리 확인 필요
+
+
+L3
+자체 LICENSE, package.json license 필드 없음. Viewer2D.js:3 에 "Ported from webviewer/…"
+루트
+사용 권리 불명확
+
+
+L4
+NanumGothic OFL 원문 미동봉
+public/licenses/
+폰트 동봉 시 위반
+
+
+B1
+?url import 가 Vite 전용
+acadrustParser.ts:6
+Node·webpack·SSR 에서 실패. Vite lib 모드로 빌드하면 wasm 이 base64 로 두 번 인라인됨 (실측 2.37MB)
+
+
+B2
+확장자 없는 상대 import
+dwgParser.ts:16, parseDxf.ts:7, Viewer2D.js:16-18 외
+Node ESM 에서 ERR_MODULE_NOT_FOUND (§3.3 과 같은 원인)
+
+
+B3
+폰트 경로 /fonts/… 하드코딩, 전역 싱글톤
+slugText.ts:32-49, Viewer2D.js:127
+호스트에 그 경로가 없으면 404
+
+
+B4
+dispose() 없음
+Viewer2D.js:159,164,4527
+익명 window 리스너와 rAF 루프를 해제할 수 없음. F 키 리스너는 입력란에서도 Fit 실행
+
+
+B5
+타입 불완전
+Viewer2D.d.ts
+load(result: unknown), 일부 메서드 누락, 기본값 주석 20 ≠ 실제 11
+
+
+B6
+패키지 설정 없음
+package.json
+private: true, exports/files/peerDependencies 없음
+
+
+B7
+소스 정본이 여러 곳
+이 레포 / hmwebviewer/src/viewer2d / hmwebviewer/packages/dwg-dxf-viewer
+wasm 바이너리 3개가 모두 다름. Viewer2D.js 는 hmwebviewer 와 1,217줄 차이
+
+
+6.3 권장 구조: 패키지 2개
+@scope/cad-parser (headless, three 없음) license: "MIT AND MPL-2.0"
+ initDwgParser({ wasm?: URL | bytes | Module })
+ parseDwg(bytes) / parseDxf(buf) / parseCad(buf, nameHint?)
+ listCadSpaces, pickDefaultSpace, isDwg/isDxf, CadParseResult 타입
+ ./worker (선택: 메인 스레드 비블로킹)
+
+@scope/viewer2d (Viewer2D, slugText, linParser) license: "MIT"
+ peerDependencies: three ^0.185.0 ← peer + external
+ dependencies: @scope/cad-parser, opentype.js
+
+파서 패키지 package.json 핵심:
+{
+ "type": "module",
+ "license": "MIT AND MPL-2.0",
+ "exports": {
+ ".": { "types": "./dist/index.d.ts", "node": "./dist/node.js", "default": "./dist/browser.js" },
+ "./worker": "./dist/worker.js",
+ "./acadrust_dwg_bg.wasm": "./dist/acadrust_dwg_bg.wasm"
+ },
+ "files": ["dist", "licenses", "THIRD-PARTY-NOTICES.md", "LICENSE"],
+ "sideEffects": false
+}
+
+wasm 로딩:
+
+기본 : glue 의 new URL('./acadrust_dwg_bg.wasm', import.meta.url) 패턴을 유지한다.
+탈출구 : init({ wasm }) 로 URL, bytes, Module 을 직접 넘길 수 있게 한다.
+Node : initSync({ module: readFileSync(...) }). scripts/parse-dwg.mjs 에서 동작을 확인했다.
+
+6.4 작업 순서
+
+라이선스 L1~L4 해결
+정본 레포 통일, rust/dwg-wasm 과 vendored acadrust 이전, rust-toolchain.toml 로 툴체인 고정
+파서 분리: ?url 제거, init({ wasm }) 추가, Node 진입점, parseCad(), 상대 import 에 .js
+Viewer2D: 생성자 옵션(fontUrl, keyboard 등), dispose(), ResizeObserver, localStorage 잔재 제거, 콜백 해제 함수
+타입: 수기 Viewer2D.d.ts 보완, CadParseResult 구체화
+빌드: tsup/rollup/tsc 로 ESM, wasm 은 파일로 복사, opentype.js 는 default import (Node ESM 에서 named import 실패 실측)
+검증: 소비 예제(Vite, webpack 5, Node)를 CI 로 돌리고, §3 의 샘플 20개 e2e 를 회귀 테스트로
+
+6.5 유의할 점
+
+크기
+
+
+
+파일
+raw
+brotli
+
+
+
+wasm
+884KB
+301KB
+
+
+폰트
+2.05MB
+432KB
+
+
+폰트는 번들하지 말고 fontUrl 옵션으로 받는다.
+
+메모리 : 37MB 도면에서 Node RSS 1.37GB, 브라우저 JS heap 782MB. wasm32 상한은 4GB 다.
+
+런타임 : CSP 에 'wasm-unsafe-eval' 이 필요하다. application/wasm MIME 이 아니면 느린 폴백으로 떨어진다.
+
+한글 : cp949/euc-kr TextDecoder 가 없는 런타임에서는 fixDwgKoreanText 가 아무 표시 없이 보정을 건너뛴다.
+
+버전 정책 : 두 패키지는 lockstep 으로 올린다. CadParseResult 필드 추가는 minor, 삭제나 변경은 major.
+
+upstream drift : README 의 "본가에서 다시 복사" 절차를 따르면 이 레포의 기능(lineweight, 2.5D, Model/Layout)이 지워진다.
+
+acadrust 버전 : 상류는 0.6.x 다. 0.4.1 fork 유지 여부를 결정해야 한다.
+
+릴리스 : 깨끗한 태그에서만 배포한다.
+
+
+
+7. 재현 방법
+# 헤드리스 파싱 (1개 파일)
+node scripts/parse-dwg.mjs "<file.dwg>" [out.json]
+
+# 기존 e2e (현재는 실패, §3.3)
+node e2e_lin_test.cjs
+
+배치 파서, 브라우저 e2e, 3단 비교 캡처 스크립트는 세션 임시 폴더에 있다. 레포 반영은 §2-3. 측정 원본은 images/sample-test-2026-10-19/results.json 에 있다.
+8. 검증 불가 목록
+
+
+
+항목
+이유
+
+
+
+OLE BMP(#3), OLE2FRAME(#4)
+OLE2FRAME 엔티티가 있는 샘플이 없음
+
+
+선가중치 배율(#11)이 AutoCAD 와 일치하는지
+같은 위치의 AutoCAD 정답 캡처가 없음
+
+
+한글 깨짐(#1) 수정 전 화면
+같은 커밋에서 고친 '글자 거대' 버그 때문에 판독 불가. 문자열 비교로 대신 검증함
+
+
+수정 전 수치와 문서 기록의 차이 (로드 213.8s vs 문서 354s, 토글 약 0.5s vs 커밋 "5초 이상")
+당시 측정 환경과 방법을 알 수 없음
+
+
+Fit 잘림의 원인
+재현만 확인, 원인 미확정
+
+
+H-DICHL06 의 patternLen(1) 과 요소 합(1.588) 중 어느 쪽이 AutoCAD 기준인지
+AutoCAD 대조 수단 없음
+
+
+문서상 한글 재현 도면 C0060203-002-굴착및시공순서도.dwg
+레포에 없음
+
+
+7~8월 테스트 당시의 증상 원문
+세션 기록이 남아 있지 않음 (가장 오래된 기록이 09-16)
+
+
+
+
+
+
+
+
diff --git a/docs/sample-test-report-2026-10-19.md b/docs/sample-test-report-2026-10-19.md
new file mode 100644
index 0000000..f96f163
--- /dev/null
+++ b/docs/sample-test-report-2026-10-19.md
@@ -0,0 +1,418 @@
+# 샘플 테스트·오류 처리 현황 보고서 (2026-10-19)
+
+> 대상: `dwg-dxf-viewer-sample` (HEAD `1f80442` + 미커밋 작업 트리)
+> 근거: 이번 재테스트 실측, git 이력 20커밋, `docs/*.md` 10건, `hmwebviewer/PROGRESS.md`
+> 상태 표기: `원인 확정` · `수정 완료·검증됨` · `수정 완료·미검증`
+> 이 보고서에는 검증된 결과만 싣는다. 검증하지 못한 항목은 §8 목록에만 적는다.
+
+---
+
+## 1. 핵심
+
+- **고유 샘플 20개(DWG 19 + DXF 1)가 모두 브라우저 뷰어에서 로드에 성공했다.** 콘솔 에러는 404 리소스 1건뿐이다. Node 헤드리스 파싱도 19/19 성공했다.
+- **기존 e2e(`e2e_lin_test.cjs`)는 지금 상태 그대로는 실패한다** (`ERR_MODULE_NOT_FOUND`). 끝까지 돌려도 assert 가 없어서 결과를 판정하지 못한다.
+- **과거에 처리한 오류 13건 중 8건을 검증했다** (§4.1). 해당 커밋의 수정 전 버전과 수정 후 버전을 실제로 띄워서, 현재 버전까지 3단으로 캡처하고 측정했다.
+ - 대용량 로드: 213.8초 → 3.6초
+ - Slug 글자 높이: 150px → 202px (비율 0.743, 커밋 주장 0.744)
+ - 레이어 토글: 454~622ms → 13~35ms
+ - 그 외 회색 원 제거, 메쉬 표시, 선가중치 적용, Model/Paper 분리, 한글 복원
+- **이번에 새로 찾은 문제는 4건이다.**
+ - Fit 이 도면 가장자리를 잘라냄
+ - `%%%` 제어코드가 변환되지 않음
+ - 기호만 있는 CP949 문자열이 깨진 채 남음
+ - e2e 스크립트가 고장 나 있음
+- **라이선스 2건 (`원인 확정`)**
+ - 공개한 MPL 패치를 원본 acadrust 0.4.1 에 적용하면 실패한다. 실제 wasm 의 소스를 재현할 수 없다는 뜻이다.
+ - Visio 저작권("All rights reserved") 라인타입 데이터가 번들에 들어 있다.
+ - Gitea 와 dist2 는 사내 전용이다. **외부 반출(고객 납품·외부 웹 공개·npm) 전에 해결해야 한다.** 라이선스 고지 버튼 자체는 형식이 맞다. 그 버튼이 가리키는 패치 내용이 틀렸다.
+- **라이브러리 배포는 조건부로 가능하다** (§6). 위 라이선스 2건, Vite 전용 코드, Viewer2D 에 `dispose()` 가 없는 문제를 먼저 해결해야 한다.
+
+## 2. 권장 액션
+
+1. **라이선스.**
+ - MPL 패치를 다시 만든다. 원본 0.4.1 과 실제 vendored 트리의 전체 diff 로 만들고, 원본에 적용해서 성공하는지 확인한다.
+ - 고지 문서 4곳의 "한 파일만 수정", "unmodified" 문구를 고친다.
+ - `ICAD_LIN` 은 IntelliCAD(ITC) 계약 범위를 확인한다. 외부 배포가 허용되지 않으면 제거한다.
+2. **미커밋 작업 정리.**
+ - 2.5D 보기, VIEWPORT 채움 변환, Z 붕괴 수정을 커밋하거나 분리한다.
+ - 루트의 untracked 대용량 파일(`C0060202.json` 31.7MB, `SAN01.json` 16.6MB, `C0060202.zip`)은 `.gitignore` 에 넣거나 삭제한다.
+3. **e2e 를 실제 테스트로.**
+ - `linParser.ts` 의 확장자 없는 import 를 고친다.
+ - 샘플 경로를 `dist/` 에서 `public/samples/` 로 바꾼다.
+ - assert 를 추가한다.
+ - 이번에 쓴 브라우저 e2e 와 3단 비교 스크립트를 레포에 넣는다.
+4. **Fit 잘림 수정** (§3.4-①).
+5. **텍스트 후처리 2건 수정.**
+ - `stripMText` 에 `%%%`, `%%nnn`, `%%o`, `%%u` 처리를 추가한다.
+ - `fixCp949Mojibake` 가 한글이 없는 CP949 기호도 복원하게 한다.
+
+---
+
+## 3. 이번 재테스트 결과
+
+### 3.1 환경
+
+| 항목 | 값 |
+|---|---|
+| 코드 | HEAD `1f80442` + 미커밋 변경(2.5D 보기, `_bakeFillXf`, `_trimmedContentBox` Z 수정) |
+| Node | v24.11.1 |
+| 브라우저 | 시스템 Chrome headless, `playwright-core@1.56.1` |
+| GPU | ANGLE D3D11 / NVIDIA RTX 3060 (실제 GPU) |
+| 타입체크 | `npm run typecheck` 통과 (컴파일 통과일 뿐, 동작 검증은 아님) |
+
+### 3.2 샘플별 결과
+
+열 설명:
+- **Node 파싱**: `parse_dwg_json` + `fixDwgKoreanText` 실행 시간.
+- **RSS**: 프로세스 전체 메모리.
+- **브라우저 로드**: 파일을 선택한 뒤 상태바에 `✓ <파일명>` 이 뜨기까지의 시간.
+- **JS heap**: `performance.memory`.
+- **notif**: acadrust notification 개수.
+
+| 파일 | MB | 버전 | 엔티티 | Node 파싱 ms | RSS MB | 브라우저 로드 ms | JS heap MB | 콘솔 에러 | notif | 결과 |
+|---|---|---|---|---|---|---|---|---|---|---|
+| BasicSample.dwg | 0.4 | R2018 | 7,309 | 142 | 147 | 553 | 47 | 1¹ | 4 | 통과 |
+| civil.dwg² | 5.4 | R2000 | 17,810 | 389 | 264 | 799 | 98 | 0 | 2 | 통과 |
+| C0060203-001-표준단면도(…).dwg | 0.4 | R2007 | 2,068 | 93 | 126 | 567 | 57 | 0 | **78** | 통과 |
+| bb.dwg | 0.7 | R2018 | 438 | 75 | 109 | 579 | 46 | 0 | 4 | 통과 |
+| simple.dxf | 0.0 | DXF | 6 | — | — | 74 | 34 | 0 | — | 통과 |
+| 04manhole1-simplify.dwg | 0.4 | R2018 | 7,035 | 118 | 138 | 575 | 54 | 0 | 4 | 통과 |
+| dwg_18.3mb.dwg | 18.4 | R2018 | 82,393 | 1,988 | 1,087 | **4,232** | 372 | 0 | 4 | 통과 |
+| test.dwg³ | 18.4 | R2018 | 82,393 | 2,156 | 1,088 | 3,914 | 371 | 0 | 4 | 통과 |
+| 대산당진 2공구_노면_….dwg | 37.1 | R2018 | 65,495 | 3,017 | **1,373** | **5,454** | **782** | 0 | 4 | 통과 |
+| a. 거더배치 삽도 | 0.2 | R2018 | 1,433 | 118 | 134 | 568 | 139 | 0 | 5 | 통과 |
+| a. 교대배치 삽도 | 0.4 | R2018 | 2,138 | 58 | 102 | 565 | 59 | 0 | 4 | 통과 |
+| a. 교량시종점 삽도 | 0.4 | R2018 | 4,432 | 84 | 105 | 542 | 56 | 0 | 4 | 통과 |
+| a. 교량폭원 삽도 | 0.5 | R2018 | 2,824 | 64 | 106 | 575 | 45 | 0 | 4 | 통과 |
+| a. 교면포장 삽도 | 0.2 | R2018 | 1,304 | 54 | 104 | 570 | 44 | 0 | 4 | 통과 |
+| a. 중앙분리대 삽도 | 0.4 | R2018 | 2,159 | 63 | 103 | 537 | 44 | 0 | 4 | 통과 |
+| a. 횡단경사 삽도 | 0.5 | R2018 | 5,635 | 126 | 138 | 569 | 44 | 0 | 4 | 통과 |
+| 산외교- ELEVATION 산출근거도 | 0.6 | R2007 | 11,262 | 162 | 181 | 937 | 111 | 0 | **44** | 통과 |
+| 산외교-받침배치도 | 0.4 | R2018 | 6,240 | 106 | 144 | 635 | 63 | 0 | 4 | 통과 |
+| 산외교_파이형_교각일반도(울산P11-꺾임) | 1.0 | R2018 | 25,927 | 371 | 219 | 810 | 56 | 0 | 4 | 통과 |
+| 산외교_파이형_교각일반도(울산P9) | 1.0 | R2018 | 26,207 | 397 | 220 | 744 | 57 | 0 | 4 | 통과 |
+
+¹ 404 리소스 1건, 첫 페이지에서만 발생. `/favicon.ico` 가 404 를 반환하는 것은 확인했다.
+² `civil.dwg` 와 `C0060202-001-평면및종단면도(…).dwg` 는 md5 가 같다. 같은 파일이다.
+³ `test.dwg` 는 `dwg_18.3mb.dwg` 와 바이트는 다르지만, 파싱 결과의 엔티티 수와 텍스트 통계가 같다.
+
+"통과" 의 기준: 상태바에 `✓ <그 파일명> · … entities` 가 표시되고, `.err` 가 없고, 페이지 crash 가 없을 것.
+
+### 3.3 기존 e2e (`e2e_lin_test.cjs`)
+
+| 실행 방식 | 결과 |
+|---|---|
+| `node e2e_lin_test.cjs` (그대로) | **실패, exit 1**. 4단계에서 `ERR_MODULE_NOT_FOUND: …/src/viewer2d/linData` |
+| resolve hook 추가 | 끝까지 실행, exit 0. H-DICHL06/06R/02R/01R/05 패턴 5개 출력 |
+
+`원인 확정`: `linParser.ts` 가 `./linData` 를 확장자 없이 import 한다. Vite 는 이것을 해석하지만 Node ESM 은 해석하지 못한다.
+
+그 밖의 문제:
+- **assert 가 없다.** 패턴을 못 찾아도 `console.error` 만 찍고, 마지막에 항상 `COMPLETED SUCCESSFULLY` 를 출력한다.
+- 샘플 경로가 gitignore 된 `./dist/samples/…` 다. clean checkout 에서는 실패한다.
+- 느린 경로인 `parse_dwg` 를 쓰고 `fixDwgKoreanText` 를 적용하지 않는다. 그래서 출력되는 라인타입 설명이 `LÇüÃø±¸` 처럼 깨진다.
+- `Found 0 text styles` 가 출력된다. `tables.styles` 가 비어 있다는 문서상 과제가 C0060202 에서도 재현된다.
+
+### 3.4 이번에 새로 확인한 문제
+
+**① Fit 이 도면 가장자리를 잘라냄.** 재현 확인.
+- ELEVATION 도면에서 Fit 을 하면 도곽 왼쪽이 화면 밖으로 나가고, 오른쪽에는 약 470px 가 비어 있다 (§4.1 캡처).
+- 자동 Fit, Fit 버튼, 창 크기 변경 후 Fit 모두 같은 결과다.
+- HEAD(미커밋 변경 제외)에서도 똑같다. WIP 회귀가 아니다.
+
+**② AutoCAD 제어코드 `%%%` 등이 변환되지 않음.** `원인 확정`.
+- C0060203 에 `-2 %%%`, civil Layout 에 `S=0.0000%%%` 가 그대로 표시된다.
+- [`Viewer2D.js:64`](../src/viewer2d/Viewer2D.js) 의 `stripMText` 는 `%%d`, `%%p`, `%%c` 만 처리한다.
+
+**③ 기호만 있는 CP949 문자열이 깨진 채 남음.** `원인 확정`.
+- civil 에 `500¡¿350` 이 표시된다. CP949 로 디코딩하면 `500×350` 이다.
+- [`fixDwgKoreanText.ts:64-67`](../src/viewer2d/fixDwgKoreanText.ts) 는 디코딩 결과에 한글이 생길 때만 변환을 받아들인다.
+
+**④ e2e 스크립트 고장.** §3.3 참고.
+
+**기타 (경미)**
+- Layout 탭 이름이 `Layout (Paper_Space0)` 처럼 내부 블록명으로 나온다. dwg2model 삽도 7개 전부가 그렇다.
+- Layout 탭이 많으면 툴바가 줄바꿈되면서 상태바가 속성 패널과 겹친다.
+- `parse_dwg_json` 은 acadrust notification 의 개수만 돌려주고 내용은 돌려주지 않는다 (C0060203 78건, ELEVATION 44건).
+
+---
+
+## 4. 처리된 오류 이력
+
+표기: [M] 커밋 메시지 · [D] diff 로 확인 · [Doc] 문서 기록.
+Rust 쪽 수정(dwg-wasm, acadrust)은 `hmwebviewer/rust/dwg-wasm` 에 있다.
+
+| # | 시기 | 증상 | 재현 샘플 | 원인 | 조치 | 커밋 | 이번 검증 |
+|---|---|---|---|---|---|---|---|
+| 1 | 07-30 | 구형 한글 TEXT/MTEXT 깨짐 | civil 외 | CP949 바이트가 Latin-1 로 올라옴 [Doc] | `fixDwgKoreanText.ts` (뷰어) | be8d52f | **`수정 완료·검증됨`** (§4.1-①) |
+| 2 | 07-30 | Model 과 Paper 동시 렌더, Layout 비어 보임, Fit/좌표, 글자 거대, VP 순서 (6건) | C0060202 | VIEWPORT 미투영, minTextH 등 [Doc] | `cadSpaces.ts`, Viewer2D (뷰어) | be8d52f | **`수정 완료·검증됨`** (§4.1-②) |
+| 3 | 07-30 | Brave/Firefox 에서 OLE BMP 미표시 | — | ` ` 가 data:image/bmp 미지원 [M] | 순수 JS BMP 디코더 [D] | 963a903 | §8 |
+| 4 | 07-30 | OLE2FRAME 이 하드코딩된 좌표에 고정됨 | — | 정답 좌표 하드코딩 [D] | 파서가 준 ul/lr 사용 | 76f3e20 | §8 |
+| 5 | 08-03 | Slug 텍스트 cap-height 약 26% 작음 | BasicSample | 3d3e052 의 회귀 [D] | 되돌림 | fb8f7ca | **`수정 완료·검증됨`** (§4.1-④) |
+| 6 | 08-03 | 19MB DWG 로딩 시 탭 프리즈 | dwg_18.3mb | wasm 힙 O(n²) [M] | `parse_dwg_json` 스트리밍 | b047dc4 | **`수정 완료·검증됨`** (§4.1-③) |
+| 7 | 08-03 | 도면 위아래 거대 회색 원 | dwg_18.3mb | `sample_arc` 각도 swap 등 [Doc] | 손상 호 감지 후 생략 (파서) | b047dc4 | **`수정 완료·검증됨`** (§4.1-③) |
+| 8 | 08-04 | 레이어 토글 지연 | dwg_18.3mb | 토글마다 `load()` 재빌드 [M] | setDrawRange, `.visible` 토글 | 3480945 | **`수정 완료·검증됨`** (§4.1-⑦) |
+| 9 | 08-04 | 노면/부체도로면(MESH) 미표시 | 대산당진 노면 | MESH 직렬화 누락, read_mesh 100k 상한 [M] | acadrust 패치 + 직렬화 + `_meshSegs` | eec3382, c67a803 | **`수정 완료·검증됨`** (§4.1-⑤) |
+| 10 | 08-04 | 모든 선이 1px | 대산당진 노면 | lineWeight 누락, WebGL linewidth 무시 [M] | LineSegments2 버킷 | c67a803 | **`수정 완료·검증됨`** (§4.1-⑥) |
+| 11 | 08-04~05 | 선가중치 배율 (8 → 20 → 11 px/mm) | 대산당진 노면 | 배율 [M] | 기본값 11, 옵션 패널 | cf12987, a924b1e, 1f80442 | 굵기 적용 확인 (§4.1-⑥). 배율 일치는 §8 |
+| 12 | 08-04 | MPL-2.0 고지 누락 | — | 고지/원문/소스 경로 없음 [M] | NOTICES, `public/licenses/` | ce3859c | **조치 불충분**: 패치가 원본에 적용되지 않음 (§5 A-1) |
+| 13 | 08-04 | `vite build` 가 dist2 테스트 파일을 삭제 | — | `emptyOutDir` 기본값 [M] | `--emptyOutDir false` 절차 문서화 | 6f200ef | — |
+
+분리 전 hmwebviewer 에서 처리한 것 (07-10~14, `PROGRESS.md`):
+- solid HATCH 구멍, HIDDEN/CENTER 파선 무시
+- MAX_TEXTS 3000 상한
+- 회전 도곽 viewTwist, 로고 해치, 폴리라인 폭, MTEXT 배경
+
+**미커밋 수정 2건** (`수정 완료·미검증`):
+- VIEWPORT 안 SOLID/HATCH 채움에 model→paper 변환 추가 (`_fillXf`/`_bakeFillXf`)
+- `_trimmedContentBox` 의 Z 붕괴 수정
+
+### 4.1 수정 전 / 수정 후 / 현재 비교 캡처
+
+**방법**
+- 각 버전은 `git archive <커밋>` 으로 풀어서 실행했다. wasm 이 커밋마다 함께 커밋돼 있어서, 그 시점의 파서와 뷰어가 같이 재현된다.
+- 계측용으로 두 가지만 덧붙였다. 둘 다 동작에는 영향이 없다.
+ - `main.ts` 에 `window.__viewer = viewer` 1줄
+ - vite 캐시 경로 분리
+- "현재" 는 오늘의 작업 트리다.
+- 로드 판정은 상태바가 `✓ <그 파일명> ·` 으로 바뀌었는지로 했다.
+- 확대 화면은 모든 버전에서 같은 WCS 좌표와 화면 폭으로 카메라를 고정했다. 파일럿 캡처로 목표 객체가 화면 중앙에 오는 것을 먼저 확인했다.
+- UI 패널은 숨기고 캔버스만 찍었다. 해상도 1280×800, RTX 3060.
+- 24회 실행 모두 로드 성공, 콘솔 에러 0건.
+
+**① 한글 깨짐** (`civil.dwg`)
+
+수정 전 버전의 wasm 이 실제로 내보낸 문자열과, 수정 후 화면을 함께 비교했다:
+
+| handle | 수정 전 `95fa6a4` wasm 출력 | 수정 후 `be8d52f` |
+|---|---|---|
+| 3232403 | `Ãø Á¡` | `측 점` |
+| 3232405 | `°èȹ°í` | `계획고` |
+| 3232408 | `Áö¹Ý°í` | `지반고` |
+| 3240601 | `õÀÇ1Áö±¸ °èȹ¼±` | `천의1지구 계획선` |
+| 3241973 | `(»ç¾÷ ÁøÇà ¿¹Á¤)` | `(사업 진행 예정)` |
+| 1009 | `°Ç¼³´Ü°è` | `건설단계` |
+
+| 수정 후 `be8d52f` | 현재 |
+|---|---|
+|  |  |
+
+**② Model/Paper 겹침, Layout 비어 보임** (`civil.dwg`, 로드 직후 기본 화면)
+
+| 수정 전 `95fa6a4` | 수정 후 `be8d52f` | 현재 |
+|---|---|---|
+|  |  |  |
+| 점 3개만 보임. Model(x≈600k·162k)과 Paper(원점)를 한꺼번에 Fit 함 | Layout 이 열리고 VIEWPORT 내용이 투영됨 | 같음 (밝은 종이 배경) |
+
+**③ 대용량 로딩 + 거대 회색 원** (`dwg_18.3mb.dwg`, 로드 직후 기본 화면)
+
+| 수정 전 `7a6edd4` | 수정 후 `b047dc4` | 현재 |
+|---|---|---|
+|  |  |  |
+| 로드 **213.8초**. 회색 SOLID HATCH 가 도면 아래쪽 절반을 덮음 | **3.6초**, 회색 채움 없음 | **3.7초** |
+
+**④ Slug 텍스트 크기** (`BasicSample.dwg`, "정면도" h=140 주변 고정 화면)
+
+| 수정 전 `b047dc4` | 수정 후 `fb8f7ca` | 현재 |
+|---|---|---|
+|  |  |  |
+| 글자 높이 **150px** | **202px** | **202px** |
+
+- 자홍색 픽셀 bbox 높이를 쟀다. 비율 150/202 = **0.743** 으로, 커밋 메시지 "3.6 → 2.68 (0.744)" 와 일치한다.
+- 주변 선과 치수의 위치는 세 장 모두 같다.
+
+**⑤ SubDMesh(MESH) 미표시** (노면 도면, 연결부 (156542, 479804), 폭 160)
+
+| 수정 전 `6f200ef` | 수정 후 `c67a803` | 현재 |
+|---|---|---|
+|  |  |  |
+| 삼각망 없음 | 도로면·부체도로면 삼각망 표시 | 같음 |
+
+c67a803 에는 선가중치 수정도 함께 들어 있다.
+
+**⑥ 선가중치** (⑤와 같은 화면)
+
+| 수정 전 `eec3382` | 수정 후 `1f80442` (11 px/mm) | 현재 |
+|---|---|---|
+|  |  |  |
+| 0.50mm 도로마킹 포함 모든 선 1px (메쉬 렌더러 이전 버전) | 굵게 표시 | 같음 |
+
+**⑦ 레이어 토글 속도** (`dwg_18.3mb.dwg`, `setHiddenLayers(CF-PATT)` 숨김→표시 3회, 동기 실행 시간)
+
+| | 수정 전 `fb8f7ca` | 수정 후 `3480945` | 현재 |
+|---|---|---|---|
+| 숨김 ms | 565 / 454 / 512 | 18 / 17 / 13 | 21 / 18 / 13 |
+| 표시 ms | 501 / 622 / 609 | 26 / 5 / 6 | 35 / 5 / 5 |
+
+**새로 확인한 문제 (현재 버전)**
+
+| `%%%` 미변환 (C0060203) | CP949 기호 잔존 (civil) | Fit 잘림 (ELEVATION) |
+|---|---|---|
+|  |  |  |
+| `-2 %%%` (정답 `-2 %`) | `500¡¿350` (정답 `500×350`) | 왼쪽 잘림, 오른쪽 약 470px 공백 |
+
+---
+
+## 5. 남은 과제
+
+### A. 우선 처리
+
+| # | 과제 | 근거 |
+|---|---|---|
+| A-1 | **MPL 패치가 실제 소스를 재현하지 못함** (`원인 확정`) - `public/licenses/acadrust-0.4.1-read_mesh.patch` 를 원본 acadrust 0.4.1 crate 에 `git apply --check` 하면 `entities.rs:2350`(read_ole2frame)에서 실패한다. - 고지의 "One file differs", "exact Source Code Form" 문구와 `acadrust-dwg/package.json` 의 "used unmodified" 가 사실과 다르다. | 이번 실행, `THIRD-PARTY-NOTICES.md` |
+| A-2 | **Visio 저작권 데이터가 번들됨** - `linData.ts` 의 `ICAD_LIN` 헤더가 "Copyright (c) 1998 by Visio Corporation. All rights reserved" 다. - `linParser.ts:228` 이 import 될 때 등록하므로 배포본에 반드시 들어간다. - IntelliCAD(ITC) 계약이 자사 웹 뷰어로의 외부 배포를 허용하는지 확인이 필요하다. | 이번 확인 |
+| A-3 | 자체 `LICENSE` 파일 없음, NanumGothic OFL 원문 미동봉 | §6 |
+| A-4 | 자동 판정 테스트 없음, e2e 고장 | §3.3 |
+| A-5 | Fit 이 가장자리를 잘라냄 | §3.4-① |
+| A-6 | 미커밋 WIP 정리, `src/topProgress.ts` 어디서도 import 되지 않음, 루트 대용량 untracked 파일 | `git status` |
+| A-7 | 원본 hmwebviewer 에 Model/Paper 수정 미이식 (`cadSpaces.ts` 없음) | `model-paper-space.md` §9 |
+
+### B. 기능·품질
+
+| # | 과제 | 근거 |
+|---|---|---|
+| B-1 | 해치 손상 호의 근본 원인 미확인. 현재 수정은 감지 후 생략. `spurious_r` 5배 임계값은 경험값 | `hatch-boundary-arc-artifact.md` §7 |
+| B-2 | 파싱 중 메인 스레드 블로킹. 37MB 도면 로드 5.5초 동안 UI 정지. Web Worker 미적용 | 이번 측정 |
+| B-3 | 메쉬 레이어에 굵기가 지정되면 fat 버킷 상한(40만) 124.5% 초과 → 버킷 전체 헤어라인 폴백 | `improvement-results` §5-3 |
+| B-4 | `%%%` 등 제어코드, CP949 기호 | §3.4-②③ |
+| B-5 | `tables.styles` 비어 있음 → 폰트 매핑 불가 | 한글 문서 §8, 이번 e2e |
+| B-6 | 한글 처리가 JS 휴리스틱. 932/936/950 미지원 | 한글 문서 §8 |
+| B-7 | Layout 탭 캡션이 내부명 | 이번 실행 |
+| B-8 | acadrust notification 내용 비노출 | 이번 실행 |
+| B-9 | 40만 상한 초과 시 사용자 알림 없음 (`console.warn` 만) | `lineweight-rendering.md` §6 |
+| B-10 | wasm32 4GB 한계. 37MB 도면에서 Node RSS 1.37GB, 브라우저 JS heap 782MB | 이번 측정 |
+
+### C. 나중에
+
+- 선가중치: 중첩 ByBlock 미추적, DXF LAYER 코드 370 무시, `celweight` 가 DWG 인덱스 그대로 나감
+- MESH: Catmull-Clark(`subdivisionLevel > 0`), POLYFACE/POLYGON MESH, shaded 패스
+- VIEWPORT: 솔리드 HATCH 근사, 비직교 VP, TILEMODE/CTAB 휴리스틱
+- OLE BMP 가 24/32bpp 만 지원
+- `parse_dwg` export 미정리
+- CXGLOGO-EX흑백.dwg 의 얇은 원 (hmwebviewer 시절부터)
+
+### D. 배포·문서 정리
+
+- `build:dist2` 스크립트 없음, `dist2/assets` 에 예전 해시 번들 누적, 테스트 도면이 `dist2/samples` 에 섞여 있음
+- 문서 상태 표기가 낡음:
+ - `subdmesh-rendering.md`: "렌더러 미커밋"
+ - `lineweight-rendering.md`, `improvement-results`: "미푸시"
+ - 여러 문서: "acadrust 미수정"
+- `Viewer2D.d.ts:24`, `Viewer2D.js:1389` 주석은 "Default 20" 인데 실제 기본값은 11
+- README 이슈 표에 hatch, large-dwg 문서 누락
+
+---
+
+## 6. 라이브러리 배포 가능성
+
+### 6.1 결론: 조건부 가능
+
+- 파서는 DOM 없이 동작한다. Node 24 에서 `initSync` 로 19개 도면을 파싱해 실측했다.
+- Viewer2D 가 DOM 에 닿는 곳은 생성자로 받는 `container` 하나뿐이다.
+- 다만 아래 차단 요소를 먼저 풀어야 한다.
+
+### 6.2 차단 요소
+
+| # | 문제 | 위치 | 영향 |
+|---|---|---|---|
+| L1 | MPL 패치로 소스 재현 불가 | §5 A-1 | 법적 의무 미충족 |
+| L2 | Visio 저작권 lin 데이터 | `linData.ts:2` | 재배포 권리 확인 필요 |
+| L3 | 자체 `LICENSE`, `package.json` `license` 필드 없음. `Viewer2D.js:3` 에 "Ported from webviewer/…" | 루트 | 사용 권리 불명확 |
+| L4 | NanumGothic OFL 원문 미동봉 | `public/licenses/` | 폰트 동봉 시 위반 |
+| B1 | `?url` import 가 Vite 전용 | `acadrustParser.ts:6` | Node·webpack·SSR 에서 실패. Vite lib 모드로 빌드하면 wasm 이 base64 로 두 번 인라인됨 (실측 2.37MB) |
+| B2 | 확장자 없는 상대 import | `dwgParser.ts:16`, `parseDxf.ts:7`, `Viewer2D.js:16-18` 외 | Node ESM 에서 `ERR_MODULE_NOT_FOUND` (§3.3 과 같은 원인) |
+| B3 | 폰트 경로 `/fonts/…` 하드코딩, 전역 싱글톤 | `slugText.ts:32-49`, `Viewer2D.js:127` | 호스트에 그 경로가 없으면 404 |
+| B4 | `dispose()` 없음 | `Viewer2D.js:159,164,4527` | 익명 window 리스너와 rAF 루프를 해제할 수 없음. F 키 리스너는 입력란에서도 Fit 실행 |
+| B5 | 타입 불완전 | `Viewer2D.d.ts` | `load(result: unknown)`, 일부 메서드 누락, 기본값 주석 20 ≠ 실제 11 |
+| B6 | 패키지 설정 없음 | `package.json` | `private: true`, `exports`/`files`/`peerDependencies` 없음 |
+| B7 | 소스 정본이 여러 곳 | 이 레포 / `hmwebviewer/src/viewer2d` / `hmwebviewer/packages/dwg-dxf-viewer` | wasm 바이너리 3개가 모두 다름. `Viewer2D.js` 는 hmwebviewer 와 1,217줄 차이 |
+
+### 6.3 권장 구조: 패키지 2개
+
+```
+@scope/cad-parser (headless, three 없음) license: "MIT AND MPL-2.0"
+ initDwgParser({ wasm?: URL | bytes | Module })
+ parseDwg(bytes) / parseDxf(buf) / parseCad(buf, nameHint?)
+ listCadSpaces, pickDefaultSpace, isDwg/isDxf, CadParseResult 타입
+ ./worker (선택: 메인 스레드 비블로킹)
+
+@scope/viewer2d (Viewer2D, slugText, linParser) license: "MIT"
+ peerDependencies: three ^0.185.0 ← peer + external
+ dependencies: @scope/cad-parser, opentype.js
+```
+
+파서 패키지 `package.json` 핵심:
+
+```json
+{
+ "type": "module",
+ "license": "MIT AND MPL-2.0",
+ "exports": {
+ ".": { "types": "./dist/index.d.ts", "node": "./dist/node.js", "default": "./dist/browser.js" },
+ "./worker": "./dist/worker.js",
+ "./acadrust_dwg_bg.wasm": "./dist/acadrust_dwg_bg.wasm"
+ },
+ "files": ["dist", "licenses", "THIRD-PARTY-NOTICES.md", "LICENSE"],
+ "sideEffects": false
+}
+```
+
+wasm 로딩:
+1. **기본**: glue 의 `new URL('./acadrust_dwg_bg.wasm', import.meta.url)` 패턴을 유지한다.
+2. **탈출구**: `init({ wasm })` 로 URL, bytes, Module 을 직접 넘길 수 있게 한다.
+3. **Node**: `initSync({ module: readFileSync(...) })`. `scripts/parse-dwg.mjs` 에서 동작을 확인했다.
+
+### 6.4 작업 순서
+
+1. 라이선스 L1~L4 해결
+2. 정본 레포 통일, `rust/dwg-wasm` 과 vendored acadrust 이전, `rust-toolchain.toml` 로 툴체인 고정
+3. 파서 분리: `?url` 제거, `init({ wasm })` 추가, Node 진입점, `parseCad()`, 상대 import 에 `.js`
+4. Viewer2D: 생성자 옵션(`fontUrl`, `keyboard` 등), `dispose()`, `ResizeObserver`, `localStorage` 잔재 제거, 콜백 해제 함수
+5. 타입: 수기 `Viewer2D.d.ts` 보완, `CadParseResult` 구체화
+6. 빌드: tsup/rollup/tsc 로 ESM, wasm 은 파일로 복사, `opentype.js` 는 default import (Node ESM 에서 named import 실패 실측)
+7. 검증: 소비 예제(Vite, webpack 5, Node)를 CI 로 돌리고, §3 의 샘플 20개 e2e 를 회귀 테스트로
+
+### 6.5 유의할 점
+
+- **크기**
+
+ | 파일 | raw | brotli |
+ |---|---|---|
+ | wasm | 884KB | 301KB |
+ | 폰트 | 2.05MB | 432KB |
+
+ 폰트는 번들하지 말고 `fontUrl` 옵션으로 받는다.
+- **메모리**: 37MB 도면에서 Node RSS 1.37GB, 브라우저 JS heap 782MB. wasm32 상한은 4GB 다.
+- **런타임**: CSP 에 `'wasm-unsafe-eval'` 이 필요하다. `application/wasm` MIME 이 아니면 느린 폴백으로 떨어진다.
+- **한글**: `cp949`/`euc-kr` TextDecoder 가 없는 런타임에서는 `fixDwgKoreanText` 가 아무 표시 없이 보정을 건너뛴다.
+- **버전 정책**: 두 패키지는 lockstep 으로 올린다. `CadParseResult` 필드 추가는 minor, 삭제나 변경은 major.
+- **upstream drift**: README 의 "본가에서 다시 복사" 절차를 따르면 이 레포의 기능(lineweight, 2.5D, Model/Layout)이 지워진다.
+- **acadrust 버전**: 상류는 0.6.x 다. 0.4.1 fork 유지 여부를 결정해야 한다.
+- **릴리스**: 깨끗한 태그에서만 배포한다.
+
+---
+
+## 7. 재현 방법
+
+```powershell
+# 헤드리스 파싱 (1개 파일)
+node scripts/parse-dwg.mjs "" [out.json]
+
+# 기존 e2e (현재는 실패, §3.3)
+node e2e_lin_test.cjs
+```
+
+배치 파서, 브라우저 e2e, 3단 비교 캡처 스크립트는 세션 임시 폴더에 있다. 레포 반영은 §2-3. 측정 원본은 `images/sample-test-2026-10-19/results.json` 에 있다.
+
+## 8. 검증 불가 목록
+
+| 항목 | 이유 |
+|---|---|
+| OLE BMP(#3), OLE2FRAME(#4) | OLE2FRAME 엔티티가 있는 샘플이 없음 |
+| 선가중치 배율(#11)이 AutoCAD 와 일치하는지 | 같은 위치의 AutoCAD 정답 캡처가 없음 |
+| 한글 깨짐(#1) 수정 전 화면 | 같은 커밋에서 고친 '글자 거대' 버그 때문에 판독 불가. 문자열 비교로 대신 검증함 |
+| 수정 전 수치와 문서 기록의 차이 (로드 213.8s vs 문서 354s, 토글 약 0.5s vs 커밋 "5초 이상") | 당시 측정 환경과 방법을 알 수 없음 |
+| Fit 잘림의 원인 | 재현만 확인, 원인 미확정 |
+| H-DICHL06 의 `patternLen`(1) 과 요소 합(1.588) 중 어느 쪽이 AutoCAD 기준인지 | AutoCAD 대조 수단 없음 |
+| 문서상 한글 재현 도면 `C0060203-002-굴착및시공순서도.dwg` | 레포에 없음 |
+| 7~8월 테스트 당시의 증상 원문 | 세션 기록이 남아 있지 않음 (가장 오래된 기록이 09-16) |
diff --git a/scripts/parse-dwg.mjs b/scripts/parse-dwg.mjs
new file mode 100644
index 0000000..c042e4a
--- /dev/null
+++ b/scripts/parse-dwg.mjs
@@ -0,0 +1,44 @@
+#!/usr/bin/env node
+/**
+ * Parse a DWG in Node — no browser, no Viewer2D. Same acadrust wasm and
+ * Korean-text fixup as src/viewer2d/dwgParser.ts.
+ *
+ * Usage:
+ * node scripts/parse-dwg.mjs # print summary
+ * node scripts/parse-dwg.mjs out.json # also write full parseResult
+ *
+ * Needs Node >= 22.18 (imports fixDwgKoreanText.ts via built-in type stripping).
+ */
+import { readFileSync, writeFileSync } from 'node:fs';
+import { initSync, parse_dwg_json } from '../src/viewer2d/acadrust-dwg/acadrust_dwg.js';
+import { fixDwgKoreanText } from '../src/viewer2d/fixDwgKoreanText.ts';
+
+const [inPath, outPath] = process.argv.slice(2);
+if (!inPath) {
+ console.error('usage: node scripts/parse-dwg.mjs [out.json]');
+ process.exit(1);
+}
+
+const wasmUrl = new URL('../src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm', import.meta.url);
+initSync({ module: readFileSync(wasmUrl) });
+
+const t0 = performance.now();
+const result = fixDwgKoreanText(JSON.parse(parse_dwg_json(readFileSync(inPath))));
+const ms = performance.now() - t0;
+
+const entities = result.entities ?? [];
+const byType = {};
+for (const e of entities) byType[e.type] = (byType[e.type] ?? 0) + 1;
+
+console.log(`file : ${inPath}`);
+console.log(`version : ${result.version}`);
+console.log(`parse : ${ms.toFixed(0)} ms`);
+console.log(`entities : ${entities.length}`);
+console.table(Object.entries(byType).sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count })));
+console.log(`tables : ${Object.keys(result.tables ?? {}).join(', ')}`);
+if (result.stats) console.log('stats :', result.stats);
+
+if (outPath) {
+ writeFileSync(outPath, JSON.stringify(result, null, 2));
+ console.log(`wrote : ${outPath}`);
+}