# [완료 계획서] B04 하단 2D 지도 — 도엽 등고선 렌더링 성능 개선 - **완료 일시**: 2026-07-28 - **이관 일시**: 2026-07-28 - **검증 문서**: `file:///D:/02_Software_Prog/%EC%9E%84%EB%8F%84%EC%84%A4%EA%B3%84%20%EB%B0%8F%20%EA%B2%AC%EC%A0%81%EC%9E%90%EB%8F%99%ED%99%94%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%9C%EB%B0%9C/docs/raw/verification/2026-07-28_verify_B04_Map_Performance.md` --- ### B04 하단 2D 지도 — 도엽 등고선 렌더링 성능 개선 (2026-07-28 협의) **배경** - B04 하단 2D 지도(`B04_wf1_Surface_UI_MapViewer.ts`)에서 도엽 등고선 켜면 팬/줌 버벅임 심각. - 사용자 조건: ① 화면 해상도 저하 절대 불가(해상도 민감), ② 등고선·세류 원본 데이터는 차기 배수유역도 기능에서 사용 예정이므로 **원본 GeoJSON·DB 무가공** — 모든 최적화는 프론트 렌더링 계층에서만. - 진행 방식: 단계별 적용 → 각 단계마다 사용자 육안 확인 후 다음 단계 진행. B04 먼저, 결과 보고 타 페이지 확산 검토. **원인 진단 (코드 확인 완료)** | 문제 | 위치 | 내용 | |---|---|---| | 정점당 DOMRect 생성 | `B04_wf1_Surface_UI_MapViewer.ts:265-283` | `toCanvasPoint`가 정점마다 `getMapRect()` 호출, 매 프레임 정점 수만큼 객체 할당 | | 무스로틀 전체 재드로잉 | `B04_wf1_Surface_UI_MapViewer.ts:506-524` | wheel/pointermove 이벤트마다 전 레이어 전 정점 재드로잉, rAF 없음 | | 캔버스 버퍼 재할당 | `B04_wf1_Surface_UI_MapViewer.ts:430-441` | `drawVectorLayer` 호출마다 canvas.width/height 재설정 | | 컬링 없음 | `drawVectorLayer` 전체 | 화면 밖 피처도 전부 그림 | | 사전 투영 없음 | `drawRing`/`drawPoint` | 매 프레임 lon/lat→픽셀 변환 반복, GeoJSON 중첩 배열 순회 | **단계 1 — 무손실 최적화 (렌더 결과 픽셀 단위 동일 보장)** - [X] 1-1. 좌표 사전 투영: GeoJSON 로드 시 1회만 lon/lat→**정규화 맵 좌표(0~1, Float64Array)**로 변환해 캐시. 매 프레임엔 뷰포트·scale·offset 합성 어파인만 적용. 피처별 bbox도 이때 사전 계산. (계획 대비 변경: 기준 픽셀 좌표 대신 정규화 좌표 채택 — 리사이즈 시 재투영 불필요) - [X] 1-2. `toCanvasPoint`의 정점당 `getMapRect()` 제거: 프레임당 1회 어파인 계수(`affineOf`) 계산 후 재사용. 원 변환식과 대수적으로 동일하므로 렌더 결과 불변. - [X] 1-3. rAF 스로틀: wheel/pointermove/resize는 `scheduleDraw()`로 상태만 갱신, 프레임당 1회만 드로잉. `dispose` 시 대기 프레임 취소. - [X] 1-4. 캔버스 버퍼 재할당은 width/height/DPR이 실제로 변할 때만 수행. - [X] 1-5. 뷰포트 컬링: 사전 계산된 피처 bbox로 화면 밖 피처 스킵 (라벨 앵커 포함, 마진 32px). - [X] 1-6. 렌더 로직 분리: `B04_wf1_Surface_UI_MapRender.ts`(신규, 359줄) 렌더 엔진 / `B04_wf1_Surface_UI_MapViewer.ts`(423줄) UI·이벤트. 둘 다 700줄 이내. - [X] 1-7. `npm run typecheck` 통과, `prettier` 포맷팅, `.claude/git-autopush.ps1` 실행. - **완료 조건**: 도엽 등고선 켠 상태 팬/줌 체감 개선, 렌더 결과 기존과 픽셀 단위 동일(해상도·선 굵기·라벨 위치 불변). 사용자 육안 확인. **단계 2 — 비트맵 레이어 캐시 (구현 후 롤백, 2026-07-28 사용자 검증)** - [X] 2-1~2-2. 제스처 중 CSS transform + 오버스캔 + settle 재렌더 방식 구현했으나, 사용자 검증 결과 **롤백**: 팬 종료마다 재렌더가 체감되고, 오버스캔 범위 밖 데이터가 다시 채워질 때 딜레이 발생. 단계 3 LOD로 프레임 렌더 비용이 충분히 낮아져 매 프레임 직접 렌더(단계 1 방식)가 더 나은 것으로 확정 — 항상 완전한 화면 보장, 재생성 딜레이·가장자리 공백 원천 제거. - **결론**: 단계 1(rAF 직접 렌더) + 단계 3(LOD) 조합 채택. 비트맵 캐시 코드는 제거됨. **단계 3 — 줌별 LOD + 곡선 스무딩 (2026-07-28 사용자 지시로 방향 확정: 줌아웃=경향, 줌인=디테일)** - [X] 3-1. 연속 LOD: 로드 시 파트별 Douglas-Peucker 가중치 1회 계산(반복형, 종횡비 보정), 렌더마다 현재 줌 기준 화면 오차 0.75px 미만 정점만 생략. 줌인할수록 자동으로 원본 정점 복원. (계획 당시 "기본 꺼짐+토글" 의도였으나 사용자가 줌아웃 시 축소를 직접 지시하여 기본 적용으로 변경) - [X] 3-2. ~~줌인 곡선 스무딩(quadratic 스플라인)~~ — 구현 후 **원복** (2026-07-28 사용자 지시: "원래 데이터가 그런 거라면 보간 불필요"). 원본 정점 형상 그대로 표시. - [X] 3-3. ~~순수 팬 settle 재렌더 생략~~ — 단계 2 롤백과 함께 제거 (매 프레임 직접 렌더 구조에선 불필요). - **완료 조건**: 사용자 검증 — LOD 렌더링 감소 확인(승인), 스무딩 불필요(원복), 최종 구조 = 단계 1 + LOD. **추가 작업 (2026-07-28 사용자 지시)** - [X] 4-1. 줌인/아웃 중심을 뷰포트 중심 → 마우스 커서 위치로 변경. wheel 핸들러에서 커서 아래 지점이 고정되도록 offset 보정(`offset' = (cursor − center)·(1 − r) + offset·r`). 배경지도는 동일 offset/scale을 쓰는 CSS transform이라 자동 정합. - [X] 4-2. 지도 영역 중간 버튼 드래그 시 브라우저 기본 자동 스크롤이 페이지 스크롤과 간섭 — pointerdown에서 `button === 1`이면 `preventDefault()`로 기본 동작 차단, 지도 팬 전용으로 사용. **제약** - 원본 GeoJSON·백엔드·DB 절대 무가공 (배수유역도 대비). - 해상도 저하 감지 시 해당 단계 롤백. - Surgical Edit — MapViewer 외 파일 수정 금지 (신규 MapRender 파일 제외).