- 조작 설명을 지도 좌상단 2열 오버레이로, 상태·범례를 좌하단 오버레이로 (계획서 0-9 ⑩⑫)
- 단추를 제목행 오른쪽으로 옮기고 공용 `.ui-btn` 규격을 씀, 하단 정보행 삭제 (⑪⑬)
- LAS 등고선을 도엽과 같은 5m 단위로 그림 (⑭)
- 바탕이 LAS 면 세류선도 등고선 범위 안쪽만 그림 (⑮)
- 지도 오른쪽 위 반시계·시계 단추로 15°씩 그림 전체 회전, 집기도 따라감 (⑯)
- R·L 하한을 5m 로 채움. 다만 L 하한은 칸 입력만 막음 — 기하에 걸면 거의 곧은 자리에서
R 이 부풀어 아무것도 안 고쳤는데 노선이 바뀜 (⑰)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jsXWphgRUHAG2mFupSKPX
- 확정 지표면이 있으면 LAS 등고선을 바탕으로 씀, 없으면 도엽 등고선 유지 (계획서 0-9 ⑥)
- 등고선 가닥마다 높이값 라벨, 겹치면 건너뜀 (③)
- 등고선을 누르면 그 가닥만 도드라지고 상태줄에 높이 표기 (⑦)
- 화면에 드는 가닥 수로 등고선 간격을 고름 — 1m 자료가 선으로 뭉개지던 것 해소
- `B04_PreProcess_UI_MapRender.ts` 700줄 초과로 사전 투영을 `_Prepare.ts` 로 분리
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jsXWphgRUHAG2mFupSKPX
PLAN 0-10. 「등고선 굽기」가 확장 8회 합 19s 였는데, 실측해 보니 굽기 자체는 0.8~2.0s
이고 나머지는 **피처 4,200개를 회차마다 다시 `shape()` 로 푸는 값**이었음. 푸는 결과는
격자와 무관하므로 같은 목록·같은 하한이면 그대로 쓴다(목록 객체를 함께 들고 있어 id 가
다른 목록에 재사용되지 않음, 최근 2벌만 보관).
실측 — 같은 격자 연속 4회: 2.66 · 1.90 · 1.99 · 2.03s (2회차부터 파싱 재사용), 결과
배열은 4회 모두 동일. `build_contour_descent` 전체는 옛 코드 12.8s → 7.2s 이며
band_elevation·valid·receiver·step_length·azimuth·levels 전부 `np.array_equal` 동일.
안 한 것 — 격자가 커질 때 옛 굽기를 옮겨 붙이고 **테두리만** 굽는 안은 만들어 재 봤으나
`rasterize` 호출당 고정 비용 때문에 오히려 3.2s → 10.6s 로 느렸고, 창에 걸치는 선만
넘기도록 고쳐도 3.8s 로 손해였음(창 4개 × 166단). 그래서 걷어내고 파싱 캐시만 남김.
`levels` 는 피처 전체에서 나오므로 격자와 무관하다는 것도 확인함.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN 0-10 이어짐. 첫 판에서는 창이 모자란 단계를 **격자 전체**로 되돌렸는데, 격자가
커질수록 그 폴백이 25/91 까지 늘어 이득이 대부분 사라졌음(실측 확장 97.7s).
- 모자란 타일만 창을 4배로 넓혀 다시 잼. 단계 전체를 되돌리지 않음.
- 타일 256 → 128 (실측: 128/여유32 2.1s vs 256/여유32 2.7s).
- 넓힘 횟수를 계측 줄에 남김(폴백 대신).
자체검증 — 전체격자 5.2s → 2.1s(넓힘 15/91), 거리·목표행·목표열 전부 동일.
`build_contour_descent` 결과도 옛 코드와 완전 동일(8.0s → 5.1s). 시험 500 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN 0-10. 확장 95.8s 중 밴드별 EDT 가 50.4s 였음 — 단계마다 격자 전체를 훑는데
정작 쓰는 값은 그 단계 밴드 셀뿐임.
- 격자를 256칸 타일로 나눠 **셀이 있는 타일만** 둘레 32칸까지 잘라 EDT 실행.
`analyze_domain` 이 domain 을 안 넘겨 밴드가 전역에 흩어지므로 바운딩박스 하나로는
안 좁아짐 — 타일이라야 좁아짐.
- 창 안 최대 거리가 여유에 닿거나 창에 낮은 라인이 없으면 **그 단계를 전체 격자로 다시**
— 근사가 아니라 같은 값을 싸게 구하는 것. 폴백 횟수는 계측 줄에 찍음.
- `return_indices` 가 주는 창 좌표에 창 원점을 더해 전체 격자 좌표로 되돌림.
자체검증 — 실제 자료(745×1035·166단, EDT 도는 91단)에서 전체격자 10.8s → **2.9s**,
폴백 9/91, 거리·목표행·목표열 **전부 `np.array_equal` 동일**. `build_contour_descent`
결과(band_elevation·valid·receiver·step_length·azimuth·levels)도 옛 코드와 완전 동일.
여유·타일 선택 근거는 파일 주석의 실측표 참조. 시험 500 통과·18 건너뜀
(기존 실패 8건은 다른 환경의 JS 도우미 문제로 무관).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN 0-10. 1차 영역이 2.6초로 떨어진 뒤 남은 병목이 `expand_by_red_boundary` 102초라
그 안을 가르기 위한 계측. 로직 불변.
- 확장 회차별 시간·격자·셀 수·방향장 재사용 여부를 한 줄씩
- 격자 해석 1회: 하강 방향장(재사용/새로) / 지형 조립 / 도로 굽기 / 세류 새김 / 도달 판정
- 하강 방향장: 등고선 굽기 / 밴드 표고(EDT 1회) / 밴드별 하강거리(EDT N회) /
위치에너지 조립 / 흐름 경로
실측 로그상 방향장이 회차마다 새로 만들어지고 밴드가 100단이라, 격자 전체 EDT 가
회차당 100회씩 도는 구조로 보임 — 그 가설을 수치로 가르려는 것.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
보간 구름을 물어볼 자리 둘레 100m 로 좁힌 뒤(9317ad31), 그 둘레에
등고선이 없는 자리는 볼록껍질 밖이라 최근접 정점 값으로 빠짐. 그 값은
등고선 간격(5m)만큼 틀릴 수 있고 그러면 그 조각의 상·하류 판정이 갈림.
용화 자료에서는 안 일어났으나(판정값 일치) 다른 현장에서 조용히 갈릴 수
있어 몇 점이 빠졌는지 한 줄로 남김. 계산은 안 바뀜.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
노선 [확인] 이 293초였고 그중 285초가 배수유역 분석, 다시 그중 256초가
1차 영역 만들기였음. 범인은 ElevationSampler 하나임 —
LinearNDInterpolator 가 들로네 삼각망을 첫 호출 때 만드는데 등고선 구름을
통째로(용화 317,348점) 넘겨 그 한 번이 256~1,481초로 뛰었음(편차 큼).
표고를 묻는 자리는 도로 교차 노드에 닿은 세류 조각 몇 개뿐임. 그 조각들만
먼저 골라 그 둘레 100m 안 등고선만 남기고 삼각망을 만듦. 여유 100m 는
등고선 재샘플 간격(5m)의 스무 배라 물어볼 점은 언제나 볼록껍질 안쪽이고
보간값이 그대로임.
실측(용화 5601e828) — 보간 구름 317,348 → 4,367점,
1차 영역 256.6s → 43.8s(측정 중 기기 부하가 9배였음, 준비 5.1s → 45.7s).
결과 완전 동일 — 1차 영역 266,697㎡ · 격자 545×835 · 268,456셀 ·
노선 이탈 0m/1079m · 상류망 1 / 하류망 109 / 미연결 1153.
pytest 409 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN 0-10. 8000 에서 1회 실측 결과 재확정 체인 292.9s 중 배수유역 분석이 285.0s(97%)로
나와, 그 안쪽을 가르기 위한 마크만 추가. 로직 불변.
- B06 확정: 조회 / 기본설계 계산 / 확정 저장 / 서버 재계산 / 측구 역반영
- 서버 재계산 내부: 상세+DB 병렬 조회 / 포장·세월교 보정 / Node 번들 / 정본 저장
- 배수유역 분석 내부: _prepare(도엽 읽기·좌표변환) / preview_stages / 응답 만들기·저장
- preview_stages: 1차 영역 / 격자 확장·흐름 판정 / 흐름 강도 / 외곽선·기본 관·화살표
`_log_steps` 는 체인 모듈 것을 그대로 씀(지연 임포트로 순환 회피).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DB 가 원격이라 질의 하나가 곧 왕복 12ms 임. 서로 기다릴 이유가 없는 읽기를
한 커넥션에서 순차로 내면 그 왕복이 그대로 더해짐.
- config_db.run_with_connection: 저장소 함수를 자기 커넥션으로 돌려 gather 로
묶을 수 있게 하는 공용 헬퍼. 정의를 한 곳에 둠(drainage_context 의 _query 는
이 함수를 가리키는 이름으로 정리).
머리에 경고 적음 — 순서가 필요한 쓰기는 이걸로 묶으면 트랜잭션이 깨짐.
- B04_PreProcess_Router_Watershed._prepare: 저장경로·좌표계·지표면확정값
세 건을 gather 로. 약 24ms 절약.
common_util_auth_repository.py:318(decide_join_request)은 FOR UPDATE + 순서 있는
UPDATE 라 묶지 않음 — 읽기 블록만 대상.
자체검증 — 관 드래그 왕복 305 -> 259ms(최소 193), 관 11 · 유역 11 로 값 동일.
전체 484 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DB·파일에는 뜻 없는 자릿수가 붙어 있고(1.4000000000000001 꼴) 서버는 응답을
전혀 압축하지 않고 있었음. B05 한 번 진입에 118MB 수신(2026-09-06 실측).
- main.py: 압축 미들웨어(1KB 이상). 3D 예상형상(/corridor)은 예외 —
숫자 배열이라 절반만 줄면서 압축에 493ms 듦(18.6->9.4MB).
- common_util_json: round_floats(value, digits) + LONLAT_DIGITS 7(1.1cm) ·
METRE_DIGITS 6. 저장 파일은 그대로 두고 내려보낼 때만 줄임.
브라우저가 다시 계산에 넣는 값(종횡단 지반선)은 줄이면 안 된다고 머리에 경고 적음.
- 도엽 GeoJSON: 자릿수만 줄인 표시용 사본(.display.geojson)을 한 번 만들어 서빙.
원본이 새로 깔리면 자동 재생성, 실패하면 원본 그대로.
등고선 도엽 65.9->36.8MB, 압축까지 11.3MB.
- 배수유역 응답: 위경도 7자리. 151->100KB, 압축 22KB.
ruff 통과, 테스트 390 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 지형 파일 개수 제한 해제(정확히 1개 → 1장 이상), 한 카드에 여러 장 담기
(화면 표시 「용화_서편.las 외 1장」, 업로드는 한 장씩 차례로 전송)
- 구조화 엔진이 여러 파일을 합친 범위로 한 벌 생성 — WF1 자동 전처리·B04 재분석 모두
프로젝트 지형 파일 전부를 대상으로 실행
- 점이 1억 개를 넘으면 씨닝 — 지면 분류점은 전부 남기고 나머지만 0.5m 칸 최저점으로 축소
(용화 실측: 4,900만점 → 249만점, 지면점 1,140,716개 그대로, 1m 지면격자 표고차 0.0000m)
- 머리글만 읽어 5km 넘게 떨어진 파일은 업로드 거부 (임도는 길어도 2~3km)
- 화면 조립부 700줄 준수를 위해 terrainCoverage 를 판정 모듈로 이동
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 지시(2026-09-04) — 「3D 는 보조라 후순위. 조용히 뒤에서 뜨고 다 되면
팝업으로 알릴 것. 로딩 도넛은 진행률과 무관하게 계속 돌고 숫자만 갱신」,
그리고 「직교/원근 전환 단추는 넣음」.
진입 로딩
- 단계 5 → 4. ④ 종·횡단 자료까지 끝나면 로딩 표시를 걷어 화면을 바로 쓰게 함.
- 3D 지형은 finally 뒤 배경 작업으로 돌리고 끝나면 토스트 「3D 지형 준비 완료」.
실패해도 나머지 화면은 그대로 씀. [초기화]는 재진입이라 같은 규칙이 걸림.
- 「확정 지표면 없음」 판정을 ③ 뒤로 앞당김.
진행 서클(공용)
- 회전 껍데기(`__spin`)를 한 겹 두고 CSS 회전을 거기에만 검. 껍데기는 늘 돌고
안쪽 svg 는 12시 고정이라 호·숫자가 제자리에서 갱신됨. `is-indeterminate` 제거.
직교/원근 전환
- 카메라 두 벌을 `B05_Profile_UI_Viewer_Camera.ts` 가 쥐고 갈아 끼움. 위치·시선·
근평면·먼평면과 보이는 크기를 옮겨 화면이 튀지 않음.
- 마커 입력·구조물 클릭·커서 피벗 유틸이 카메라를 함수로 받게 바꿈(공용 유틸은
값도 그대로 받아 B04 호출부는 무변경).
자체검증(공용 브라우저 5173, 용화_LAS) — 진행률 25→50→75%, 회전 애니메이션
running 유지(변환행렬 표본 전부 다름), 로딩 걷힌 순간 그래프선 2·좌측단추 61·
3D 메쉬 2, 1.8초 뒤 메쉬 489 + 토스트. 초기화 재진입도 같음. 전환 단추는
Ortho ↔ Perspective 왕복 후 값 완전 일치, B04 페이지 오류 0건.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 계획노선 사용 범위: B02 등록에 시작·종료 누가거리 두 칸 추가, B01 수정 모달에서도
변경. projects.route_start_m·route_end_m 신설(015_route_range.sql).
load_design_route 가 범위 절단 → 서피스 트림 순서로 적용. 시작 >= 종료는 화면·서버
양쪽에서 차단. 비우면 전 구간으로 종전과 같음.
- 서피스 절단 여유 기본값 30m → 3m (SURFACE_ROUTE_EDGE_TRIM_M).
- B04 지도·B05 배수유역도 줌 상한을 「화면 폭 20m」 기준으로 계산(고정 8배·16배 폐지).
4배를 넘으면 배경 그림 흐림 보간 해제.
- 계획선 위 측점 눈금·번호 표기(측점번호+잔여거리). 관 마커와 겹치면 반대쪽으로 밀고,
되꺾임 구간에서 라벨이 겹치면 건너뜀. 그리기 코드는 두 화면 공용.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 워크플로 상태 응답에 project_name 추가. 좌측 제목 패널 같은 행 오른쪽 끝에
프로젝트 이름 표기(길면 말줄임, 전체는 툴팁). 이름표를 오버레이 쪽에 두어
레이아웃을 직접 조립하는 B03 까지 여섯 화면이 한 곳으로 반영됨.
- fetchProjectWorkflowState 가 {status, workflow_state} 껍데기를 벗기도록 수정.
- 계획노선을 「업로드한 CSV」로 적은 주석을 「계획노선(정본)」으로 정리.
업로드 판정이 .csv 개수만 세어 shapefile 을 막던 것도 .shp 포함으로 맞춤.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
700줄 분리 때 _Router_Progress · _Router_Status 로 옮겨 진입 파일에서 안 보이게 된
PROGRESS_FILE_RELATIVE · _progress_file_path · read_surface_progress ·
get_wf1_analysis_status · get_surface_model_preview 를 as 재노출로 되살림.
옛 경로를 보던 호출부·테스트가 그대로 동작하게 하려는 것(laptop-main 의 B07 사례와 같은 사고 예방).
검증: 분리 전 모듈 정의 이름을 전수 대조해 누락 0개(_Router 16개 · _Router_Watershed 30개),
라우트 9개 유지, ruff check/format 통과, tmp/tests 183 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B04_PreProcess_Router_Watershed.py 714줄 -> 518줄. 산출물 기록(.npz·스테이지 기하)과
응답 좌표 변환 헬퍼 9개를 B04_PreProcess_Router_Watershed_Output.py(225줄)로 그대로 옮김.
공개 심볼 router · get_primary_region · CONTOUR_FILE · STREAM_FILE 유지
(main.py · B03_FileInput_Service_Chain · B07_DesignDetail_Router_Support 참조).
검증: ruff check 전체 통과·format 적용, tmp/tests 183 passed, 백엔드 재시작 후
경로 2개 유지, /drainage/rainfall 200, /drainage/primary-region 200(schema_version 3),
B05 화면 유역 11개·측점 66개 정상, 옮긴 순수 헬퍼 6개 직접 호출 스모크 통과.
분석 재실행(쓰기 경로)은 작업본 산출물을 덮어써서 돌리지 않음.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B04_PreProcess_Router.py 718줄 -> 493줄.
- B04_PreProcess_Router_Status.py(221줄) — 분석 상태 조회·모델 프리뷰 엔드포인트.
prefix 없이 만들고 본체가 include_router 로 실어 붙임(본체 prefix 를 그대로 상속).
- B04_PreProcess_Router_Progress.py(47줄) — progress.json 읽기·쓰기 헬퍼.
본체와 상태 라우터가 함께 써야 해 순환 임포트를 피하려고 별도 모듈로 둠.
본체는 write_surface_progress 를 계속 재노출(B03_FileInput_Service_WF1 이 참조).
검증: ruff check/format 통과. 라우터 경로 9개가 분리 전과 동일(중복 prefix 없음 확인).
백엔드 재시작(8001) 후 openapi 에 surface 경로 12개 그대로, 브라우저에서
/surface/status 200(진행률 100·모델 3), /surface/models 200,
/surface/models/415/preview 200 model/gltf-binary 7,946,756바이트.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B04_PreProcess_UI_Style.css 870줄 -> 565줄. 하단 2D 지도·도구줄·시트 툴바 규칙을
B04_PreProcess_UI_Style_Map.css(312줄)로 값·순서 그대로 옮김. 진입 TS 에서 본체 다음에
불러 캐스케이드 순서 유지.
검증: 두 파일을 다시 이으면 원본과 **경계 빈 줄 1개 외 완전 동일**(텍스트 대조).
tsc --noEmit 통과. 공용 브라우저(5174) B04 화면 실측 — 좌측 폼 flex/gap 16px,
.b04-map flex column gap 12px(896x828), 헤더·컨트롤 정상, 3D 서피스·2D 지도 렌더 정상.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B05 3D 뷰어에는 방위 표시가 없어 방향을 읽을 수 없었음(사용자 지적, 메인 창 경유).
B04 가 오늘 새로 만든 「지면에 누운 나침반 링」을 그대로 쓰되, 두 페이지가 함께 쓰도록
공용으로 옮김.
- `B04_PreProcess_UI_Compass.ts` → `ui_template/ui_template_compass.ts` (git mv).
`createTerrainCompass({ sizePx, className })` 로 크기·자리를 호출부가 정함.
- 스타일도 `ui_template/ui_template_compass.css` 로 옮기고 모듈이 직접 물고 옴.
**놓을 자리만** 페이지 CSS 몫 — B04 우하단(`b04-surface__compass`),
B05 는 ISO 버튼 아래(`b05-route__compass`).
- B05 뷰어는 104px 로 쓰고 렌더 루프에서 카메라 오프셋으로 갱신, 지형이 없으면 감춤.
좌표 규약이 B04 와 같음(북 −z · 동 +x · 표고 +y, `Viewer.ts:295` 정점 변환) — 그대로 맞음.
화면 검증(5174): B05 나침반 104×104, ISO 버튼 아래 8px. 도북 화살이 뷰 전환에 반응 —
TOP 에서 `M0.00 -33.30`(수직), ISO 에서 `M23.56 -13.60`. 후자는 카메라 오프셋 (d,d,d)
에 대한 투영 이론값(x=R/√2=23.56, y=−R/√6=−13.60)과 일치. B04 쪽도 그대로(84×84,
우하단 16px, 링·화살 색 토큰 유지). `tsc --noEmit` 통과, 뷰어 693줄로 제한 안.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
시안 3종 비교 뒤 사용자 선택(2026-09-03) — TerriaJS·cesium-navigation 계열 표기.
링·눈금·도북 화살을 지면 평면 도형으로 두고 카메라에 투영하므로, 링이 눌린 정도가
곧 시점 기울기이고 도북은 늘 화면의 실제 북쪽을 가리킴.
- 15° 눈금(90°마다 긴 눈금), N·E·S·W 라벨(뒤로 넘어가면 흐림).
- 표고축 침은 내려다볼수록 짧아지고, 점으로 눌리면 Z 라벨을 감춤.
- 위젯 56 → 84px — 라벨이 읽히는 최소 크기.
검증: typecheck·prettier 통과. 리로드 실측 — 위젯 84×84, 링 경로 `M0.00 −33.98…`,
도북 화살 `M0.00 −33.30 L−4.28 −17.14 L4.28 −17.14Z` 로 기본 시점 계산값과 일치.
회전 중 거동은 사용자 화면 검증 대기.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
옛 콤파스는 방위각(`atan2(ox, oz)`)만 써서 경사에 따른 남북 압축이 빠졌음 —
사시도 프리셋(오프셋 120·95·135, 앙각 27.7°)에서 바늘 41.6°, 화면 실제 북쪽 62.4°로
20.8° 어긋났음(2026-09-03 사용자 지적).
세계 축 N(−z)·E(+x)·Z(+y)를 카메라 화면 기저(오른쪽 r, 위 u)에 투영해 선과 글자를
그림. 기저는 카메라 오프셋만으로 구함(up = +y 고정). 뒤로 누운 축(깊이 > 0.25)과
정면으로 와 짧아진 축은 흐리게 그려 겹침을 줄임.
검증: typecheck 통과. 기본 시점에서 DOM 좌표가 해석값과 일치 —
N (28.0, 11.0) · E (45.0, 28.0) · Z (28.0, 27.4). 회전 중 거동은 사용자 화면 검증 대기.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
같은 폴백 사다리가 네 곳에 서로 다른 모양으로 흩어져 있었고, 그중 배수유역 라우터
`_route_center_lonlat` 은 노선 CSV 의 `crs_epsg` **라벨**을 그대로 변환에 썼다(라벨과
실좌표계가 다른 사례 실측 — 2026-09-01 용화 라벨 5179 / 실제 5176).
`common_util_crs.resolve_project_crs()` 신설 — 사다리는 ① 노선 crs_input ② 파일 라벨
(원본 좌표를 읽는 자리만) ③ 지형 PRJ(작업 좌표계 정본) ④ DB epsg ⑤ EPSG:5186.
작업 좌표계는 **지형 PRJ 우선으로 확정**(2026-09-03 사용자 결정 — 서피스 격자가 모델좌표의
주인이라 현행 유지). `project_epsg_from_prj()` 도 이 창구로 위임.
더해 지표면 트림이 노선을 통째로 지울 때 노선·지표면 bbox 를 함께 로그에 남긴다 —
"겹치지 않습니다" 만으로는 좌표계 문제인지 측량 범위 문제인지 갈리지 않았다.
검증 — `tmp/tests/test_project_crs_resolution.py` 5건 신설(사다리 4·진단 1),
전체 375 passed·17 skipped, ruff format 무변경.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
단일 링 자료형이 곧 중첩·빈공간이었다. 아래 유역이 위 유역을 감싸면 구멍이 사라져 위
유역을 통째로 덮고(합성 실측 256㎡ 전량), 한 관의 유역이 두 조각이면 작은 쪽이 사라져
빈공간이 됐다(144㎡).
- `polygon_parts()` 신설 — 조각마다 [외곽 링, 구멍 링...], 넓은 조각부터.
- `WatershedBasin.boundary_parts` 로 교체. `boundary_xy`(가장 넓은 조각의 외곽)와
`boundary_rings`(편 링 목록)는 파생 속성이라 옛 소비처는 그대로 동작한다.
- API 에 `polygon_rings_lonlat` 추가. 저장 GeoJSON 은 구멍을 가진 Polygon, 조각이
여럿이면 MultiPolygon.
- 캔버스 4곳(B04 유역화면 채움·선택, B05 배수유역도 채움·선택)은 even-odd 로 한 번에
채우고 `pointInRings()` 로 판정 — 구멍 안을 눌러도 바깥 유역이 잡히지 않는다.
중복이던 지역 `pointInRing` 은 삭제하고 공용 것으로 통일.
검증: 단위 5건 신규(도넛 구멍 보존·조각 2개 보존·파생 속성·빈 경계·조립 경로 전체에서
링 2개), 전체 168 passed, typecheck 통과. 브라우저 — API 가 유역 22개에 링 목록을 실어
보내고, 캔버스 even-odd 실측(가운데 알파 0 / 고리 255)으로 구멍이 실제로 뚫린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
뷰어 좌표 규약이 `x, 높이, -y` 라 세계 북쪽은 -z 다. 카메라 오프셋(카메라 − 타깃)의
수평 성분으로 도북이 화면에서 몇 도 돌아갔는지 계산해(atan2(offset.x, offset.z)) 바늘을
돌린다. 뷰어 우하단 — 축척 막대(좌하단)의 반대편에 둔다.
뷰어 파일이 이미 946줄이라 콤파스는 별도 모듈로 분리(700줄 제한). 애니메이션 루프에서
0.5° 이상 움직였을 때만 transform 을 다시 쓴다.
검증: typecheck 통과. 공용 브라우저 실측 — [상단] 시점 0°(바늘이 북쪽=화면 위),
[사시도] 41.6°, 캔버스 220px 드래그 시 218.6° 로 카메라와 함께 회전.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 자동설계 체인이 초기값 스냅샷에 `planned_route.csv` 를 함께 남김. 값은 체인이
이미 만든 정점(사업지 좌표계·트림·조밀화 후)이라 재계산 없음.
- `load_design_route()` 는 surface_params 가 주어진 호출에 한해 그 CSV 정본을 읽음.
트림 전 원본이 필요한 호출(도엽 범위)은 종전 경로 유지. 지표면·노선이 바뀌면
`discard_initial_snapshot()` 이 폴더째 지워 경로가 저절로 닫힘.
- `run_sheet_surface_analysis()` 가 노선을 원본 좌표계로 읽어 도엽 서피스를 딴 자리에
만들던 문제 교정 — `load_design_route()` 로 창구 통일, 도엽 확보 PRJ 탐색도 지형
PRJ 폴더로 교정. shapefile 노선(5179) + 지형 PRJ(5176) 조합에서 트림이 노선을
통째로 지우던 원인.
검증: tmp/tests 158 passed(신규 4건 포함), 공용 브라우저 실사용 경로로 용화 shapefile
한 벌 업로드 → 자동 체인 완주 → CSV 정본 643점, 원본 경로와 최대 좌표차 0.00007m.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
파일마다 포맷 폭이 달라(≈80 대 100) 한 줄만 고쳐도 포맷터가 무관한 줄을 대량
재포맷했음. 사용자 지시로 전체를 한 번에 맞춤. 코드 동작 변경 없음 — 포맷만.
- 프론트엔드 `.ts/.css/.html` → 저장소 prettier (`.prettierrc`, printWidth 100)
- `B07_DesignDetail/openwebcad/**` → 자체 biome (tab 들여쓰기·single quote·lineWidth 100).
`biome format` 만 사용 — `biome lint --write` 는 포맷 아닌 코드 수정까지 하므로 제외
- 파이썬 → `ruff format` (엔진 코드는 이미 정합, resources·scratch 스크립트 24개만 변경)
두 포맷터가 서로 되돌리지 않도록 `.prettierignore` 신규 — openwebcad 와 빌드·산출물
폴더를 prettier 대상에서 뺌. `.prettierrc` 에 `endOfLine: "auto"` 추가 — 기본값 `lf` 가
`core.autocrlf=true` 로 받은 CRLF 파일을 매번 전부 다시 써서 `--list-different` 가
실제 포맷 차이를 가리고 있었음.
검증: `tsc --noEmit` 통과(루트·openwebcad 둘 다), pytest 349 passed / 17 skipped /
0 failed, CAD vitest 87건 중 81 passed / 6 failed(laptop-sub 기준선과 동일, 회귀 없음).
포맷터 재실행 시 prettier·biome 모두 변경 0건.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- B04 지표면 3D 뷰어가 지표면 범위만으로 카메라 거리를 정해, 라이다가 노선의
일부만 덮으면 나머지가 화면 밖으로 잘렸음. 카메라 타깃이 늘 지표면 중심이라
한쪽만 넓혀서는 소용이 없어, 중심에서 가장 먼 노선 정점까지를 반폭으로 잡아
대칭으로 넓힌 `fitBounds()` 를 `fitCamera()` 가 씀. 노선이 없으면 종전 동작 유지.
- 계획선 1차(배관 정착)와 2차(직선 분할 폴백)가 같은 `_profile_entry()` 를 써서
저장본 `basis` 가 둘 다 `station_alignment` 로 나갔음. `PIPE_ANCHORED_BASIS`
신규 + `_profile_entry(basis=)` 인자로 1차만 값을 가름. 2차는 종전 값 유지,
편집 재구성은 저장본 값 승계.
검증: 용화 프로젝트 공용 브라우저 실측 — 노선색 픽셀 x 범위 48~399(우측 끝에 닿음)
→ 145~386(양끝 캔버스 안). pytest 349 passed / 17 skipped / 0 failed, tsc 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
① 3D 서피스 겹쳐보기 어긋남 — `write_glb()`는 넘겨받은 상자의 중심을 원점으로 삼는데
모델마다 자기 상자를 넘겨 도엽 서피스와 라이다 지표면이 다른 원점에 섰다
(2f940d8a 실측: 수평 7.85m·높이 1.28m). 도엽 서피스가 라이다 포인트 상자
(`structured.npz`, 화면 `setReferenceBounds`와 같은 값)를 화면 원점으로 쓰게 했다.
표고 격자 상자(`bounds`)는 절취 범위 그대로 두고 `scene_bounds`를 npz에 따로 남긴다 —
등고선 API도 이 값을 먼저 써서 등고선이 메시 위에 얹힌다. 라이다 없는 사업지는 기준이
자기뿐이라 종전대로 자기 상자를 쓴다.
② 세부유역 저장본 좌표계 — `04_detailed_basins.geojson`에 변환에 쓴 좌표계를
`crs_input`으로 남기고, B07 유역도가 그 값으로 되돌린다. 기록이 없는 옛 저장본은
노선 CSV의 EPSG 라벨로 쓰였으므로 그 라벨로 되돌린다(경고 로그 + 재확정 안내).
검증: tmp/tests 42 passed (신규 test_scene_origin_and_basin_crs.py 5건).
패치 코드로 도엽 서피스를 다시 만들어 GLB 정점 상자를 대조 —
sheet [-426.58, -68.95, -386.41]~[411.42, 71.52, 379.59],
csf [-172.12, -44.06, -199.69]~[184.11, 3.22, 165.72] 로 같은 원점.
수정 전 sheet는 [-419.5, -70.24, -383.0]~[418.5, 70.24, 383.0] (자기 중심)였다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
상세 배수유역 폴리곤이 지도에 안 그려졌다. 노선 CSV `crs_epsg` 열(라벨 5179)을
좌표계로 써서, 사업지 좌표계(.prj EPSG:5176)로 계산된 격자 산출물을 잘못 역투영한
탓이다 — 유역이 lon 119.79 / lat 23.08(대만 남쪽 바다)로 나가 화면 밖이었다.
`load_design_route()`는 노선을 .prj 좌표계로 재투영하며 `crs_input`만 갱신하고
`epsg` 라벨은 CSV 값 그대로 둔다(2026-08-31 확정). 그 라벨을 좌표계로 쓰던 자리를
모두 `crs_input`(= .prj 좌표계)으로 바꿨다.
- common_util_drainage_context: DrainageContext.epsg(int) → crs(str, pyproj 입력).
상세 배수유역·관 지점 응답 좌표가 노선 위로 돌아온다.
- B07_DesignDetail_Router_Support: context.crs 그대로 사용.
- SheetSurface.build_sheet_surface_from_route: load_design_route로 노선을 읽어
도엽 서피스를 라이다 지표면과 같은 좌표계에 만든다(기존엔 5179 격자로 만들어져
라이다 DTM과 다른 프레임에 놓였다). LAS 없는 WF1 경로는 라벨·좌표가 한 벌이라 그대로.
- Router_Inflow._resolve_epsg: 격자 산출물과 같은 .prj 좌표계로 역투영.
검증: tmp/tests/test_drainage_crs.py 신규(용화 샘플 회귀) + tmp/tests 전체 37 passed.
build_detail 실행 결과 유역 11개 폴리곤이 lon 129.0947~129.0988 / lat 36.8107~36.8127
(노선 위)로 나온다 — 수정 전 같은 좌표는 119.7872 / 23.0817이었다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
자동 전처리가 기본 방식(laplace) 하나만 만들도록 바꾼 뒤, 나머지 다섯 방식을 고를
길이 없었다. 버튼을 **만들어진 모델**에서 세우고 있었기 때문이다.
- GET /surface/sheet-methods : 고를 수 있는 방식과 라벨, 생성 여부
- POST /surface/sheet-surface : 한 방식만 만들어 등록 (method 검증 후)
- save_sheet_surface_models(): 방식 하나를 덧붙이는 등록. 전체 재분석과 달리 기존
행을 지우지 않는다 — 같은 파일을 가리키는 옛 행만 걷어 낸다.
- 버튼은 이제 고를 수 있는 방식 전체로 세운다. 안 만든 방식은 점선으로 표시하고,
누르면 모달로 물은 뒤 그 방식만 만든다. 만든 뒤에는 점선을 걷는다.
- 첫 진입은 만들어 둔 방식을 고른다 — 안 그러면 열자마자 모달이 뜬다.
방식마다 파일명이 갈려 있어(dtm_sheet_{방식}.npz) 한 번 만든 방식은 다시 골라도
그대로 쓰인다(2026-09-01 사용자 확정).
라벨 정본은 백엔드 SHEET_METHOD_LABELS 하나다 — 화면에 복사해 두지 않는다.
검증(용화 e418fcfc, sheet_tin 행을 지워 재현):
버튼 6종 노출, TIN 격자만 (미생성) 표시
클릭 -> 모달 -> [계산하기] -> 55초 -> 점선 걷히고 선택됨
ruff 통과, tsc 무오류, tmp/tests 322 passed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
용화 자동 체인이 끝까지 돌았는데 종단 계획선이 PVI 3개 직선으로 나왔다. 관 22개
측점이 92.6~2044.9m 로 트림 전 원본(2,136m) 기준인데 확정 노선은 1,070.4m 라
21개가 노선 밖이었다. 좌표계도 갈려 있었다 — 관은 EPSG 5179(노선 파일), 노선은
5176(프로젝트)에 저장돼 누가거리만 우연히 맞물려 있었다.
원인은 계획노선을 읽는 곳이 흩어져 있는 것이다. 트림·조밀화를 체인 한 곳에만
넣었더니 배수유역·유입·도엽은 원본을 그대로 읽었다.
- load_design_route() 신규 — 읽기·좌표계 변환·트림·조밀화를 한 곳에서 끝낸다.
설계 계통(체인·배수유역)은 전부 이 함수를 지난다.
- Router_Watershed 도 이 함수를 쓴다. 좌표계가 프로젝트 기준으로 통일돼 관과
노선이 같은 공간에 놓인다.
- 도엽 서피스는 SHEET_SURFACE_AUTO_METHODS(기본 laplace 1종)만 자동 생성한다.
여섯 방식을 매번 만들면 WF1 891초 중 655초를 여기서 쓴다. 나머지는 관리자가
화면에서 고를 때 만든다.
Router_Inflow 는 노선을 EPSG 라벨용으로만 쓰고 기하를 안 써서 제외했다.
Engine_Extent(배경 지도 범위)도 제외 — 지도는 트림 전 전 구간을 덮어야 사용자가
측량 범위 밖을 볼 수 있다.
실측(용화 route 120):
배수유역 노선 2,136m -> 1,070m 격자 889x1435 -> 545x831
1차 영역 538,574㎡ -> 263,170㎡
관 좌표계 EPSG 5179 -> 5176
관 22개(10개가 노선 밖) -> 11개 전부 노선 안
종단 PVI 3개 -> 13개, 종곡선 1 -> 11, 불균형 97.0% -> 86.2%
PVI 측점이 관 측점과 일치한다(반올림 오차 제외).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
classification 필터를 넣은 뒤 B04 화면의 포인트클라우드 패널이 "데이터가 존재하지
않습니다"로 비었다. 원인은 화이트리스트 중복이다 — point-cloud 라우터가 필터 목록을
{"grid_min_z","csf","pmf"} 로 따로 적어 두고 있어 classification 을 400으로 막았다.
GET /surface/point-cloud?filter=classification -> 400 "지원하지 않는 지면 필터입니다."
필터 등록부는 Ground._FILTERS 하나뿐이므로 available_filters() 를 쓴다. 여기 따로
적어 두면 필터가 늘 때마다 이 화면만 막힌다.
이 400이 연쇄로 3D 노선 표시까지 막았다 — 노선 평면이 referenceBounds 를 필요로 하는데
그 값이 포인트클라우드 응답에서만 왔기 때문이다. 확정 서피스 bounds(수 KB)를 먼저
받아 두도록 분리했다. 포인트클라우드는 수십 MB(실측 28MB·9.3초)라 늦거나 실패할 수
있는데, 그때 노선까지 같이 사라지면 안 된다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
실무 자료 용화.las가 계획노선 2,136m 중 일부만 덮는다. 좌표 문제가 아니라 측량
범위 자체다 — LAS(VLR EPSG 5176)와 정사영상 용화.tif 범위가 서로 일치하고, 위경도로
datum 보정까지 태워도 위도는 완전히 포함되며 경도만 동쪽 431m 초과한다.
노선 3D 표시
- 계획노선을 지표면에 드리우지 않고 데이터 최고 표고(bounds.z_max) 평면에 수평으로
얹는다. 노선과 측량 범위가 평면상 어디서 어긋나는지 보려는 것이라 지형을 따라
오르내리면 오히려 판단이 어렵다.
- 색은 2D 지도·B05 배수유역도가 쓰는 routeLineColor()를 그대로 쓴다. 같은 선을 두
화면에서 다른 색으로 그리면 같은 것인지 알아볼 수 없다.
노선 트림
- trim_route_to_surface(): DtmGridSampler 의 valid_mask 로 판정한다. bounds 사각형이
아니라 불규칙한 실제 외곽이다. 가장 긴 연속 유효 구간을 남긴다.
- 가장자리 여유 SURFACE_ROUTE_EDGE_TRIM_M(30m)은 잘라 낸 쪽 끝에만 적용한다. 노선
본래 끝점이 지표면 안이면 깎지 않는다.
- 자르는 자리는 _planned_route_points_in_project_crs() 한 곳이다. 체인의 BP·EP·CP가
전부 이 함수를 지나므로 여기서 한 번 자르면 하류가 모두 유효해진다.
- 지표면을 못 열면 자르지 않는다. 트림 실패가 설계를 막으면 안 된다.
실측(용화 노선 2,136m):
csf/dtm/smooth -> 1,310m (61%)
classification/dtm/smooth -> 1,070m (50%)
bounds 사각형 기준 추정치 1,400m보다 짧다 — 실제 외곽이 사각형보다 작기 때문이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
csf 처리 결과를 눈으로 검증할 수 있어야 한다(사용자 지시). 지면분류가 있는 LAS는
classification 하나만 만들고 끝내면 csf가 제대로 도는지 확인할 방법이 없다.
resolve_auto_source_filters 가 이제 목록을 돌려준다 — 첫 항목이 기본 확정값이다.
분류 있음: ['classification', 'csf'] 기본 확정 classification
분류 없음: ['csf'] 기본 확정 csf
서피스 생성 로직은 두 경우가 같다. 갈리는 것은 기본 확정값 지정뿐이다.
실측: cloud_merged.las(미분류) -> ['csf'], 용화.las(class 2 2.33%) ->
['classification', 'csf'].
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
화면 검증에서 드러났다. 드롭다운으로 classification/dtm 을 요청하면 UI가 실패하고
드롭다운이 되돌아갔다. 서버는 200을 돌려주고 있었다 — 실측 114초·144초가 걸리는데
API_ANALYSIS_TIMEOUT_MS(60초)가 먼저 abort 했다.
조합 하나를 만드는 요청은 구조화된 점군(4,900만 점) 전체를 다시 보간하므로
분석용 60초로도 모자란다. API_SURFACE_BUILD_TIMEOUT_MS(15분)를 새로 두고
analyzeSurface 에만 쓴다. 사용자에게는 모달이 미리 "수 분이 걸립니다"로 알린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
지면 필터를 고쳐 지면점이 9~26배 늘자, 필터 전체 x 표현 전체를 미리 만드는
자동 전처리가 감당 못 할 만큼 길어졌다(용화 기준 15~20 모델). 산출물 대부분은
아무도 열어 보지 않는다.
- 자동 전처리는 기본 필터 1종 x SURFACE_AUTO_METHODS(dtm) 만 만든다. 스무딩
유무 두 벌은 기존대로 같이 나온다.
- 기본 필터는 고정값이 아니라 입력 LAS를 보고 정한다 — 지면분류(class 2)가
있으면 classification, 없으면 csf. csf는 분류 없는 LAS를 필터링하기 위한
수단이므로 그때만 쓴다.
- 관리자가 B04 드롭다운을 바꾸면 그 조합이 이미 저장돼 있는지 보고, 없으면
모달로 물은 뒤 그 조합만 계산해 영구 저장한다. 취소하면 드롭다운을 되돌린다.
이미 있으면 묻지 않고 저장된 데이터를 그대로 쓴다.
- config_signature에서 source_filters·precompute를 뺀다. 이 둘은 "무엇을
만들지"를 고르는 값이라 서명에 넣으면 조합을 바꿀 때마다 manifest가 통째로
폐기돼 이전에 만들어 둔 조합이 사라진다.
- analyzeSurface가 API_ANALYSIS_TIMEOUT_MS를 쓴다 — 기본 30초로는 조합 하나를
만드는 동안 abort 된다.
detect_extra_filters()는 resolve_auto_source_filters()로 대체했다. 필터를
말없이 덧붙이는 대신, 자동 경로의 기본값을 정하는 판정으로 쓴다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
실측(2026-08-31): 업로드 후 화면에서 "계획 노선 CSV를 읽지 못했습니다: ...shp"
경고가 반복됐다. find_planned_route_file 은 shapefile을 우선 돌려주는데 받는 쪽이
아직 read_planned_route_csv 였다 - 확장자 분기가 없는 판독기라 .shp 를 CSV로 열다
실패하고 노선 없이 진행했다.
앞선 커밋에서 Service_Chain, SheetSurface, Router_GIS 는 바꿨으나 아래 세 곳을
놓쳤다(조사 당시 grep 출력이 잘려 목록에서 빠졌다):
- B04_PreProcess_Router_Watershed.py (2곳)
- B04_PreProcess_Router_Inflow.py
- common_util_drainage_context.py
셋 다 read_planned_route 로 바꿨다 - CSV/shapefile을 확장자로 갈라 읽는다.
검증: ruff check 통과, tmp/tests 302 passed (잔여 실패 11건은 기존 실패로
HEAD 사본에서 동일 재현).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
원청 정식 계획노선이 shapefile(UTM-K)로, 지형이 별도 PRJ(동부원점 Bessel)로
들어오는데 입력 경로가 shapefile 확장자를 막고 PRJ를 프로젝트당 1개로 전제했다.
- 업로드 허용에 .shp/.shx/.dbf/.cpg 추가, 한 번에 보낼 파일 수 5 -> 10
- B03_FileInput_Engine_Shapefile: ESRI 규격 직접 파싱(GDAL 미사용). 형제 파일이
아직 안 왔어도 .shp 하나로 기하를 읽는다. .cpg 내용이 949뿐인 실물을 CP949로
정규화해 한글 속성을 살린다.
- 노선 판독을 read_planned_route로 일원화(CSV/shapefile), PlannedRoute에
crs_input 추가 - 변환 입력은 EPSG 코드가 아니라 crs_input_from_prj가 주는
값(EPSG:n 또는 원문 WKT)이다. 실물 PRJ 2종 모두 to_epsg가 None이다.
- shapefile 세트를 input/shp/ 한 폴더에 모은다(GDAL 요건). 노선 PRJ가 그 안에
남으므로 지형 PRJ(input/prj/)와 파일명 정렬 운에 기대지 않고 갈린다.
find_project_prj가 지형 PRJ를 프로젝트 좌표계로 고른다.
- 필수 세트를 노선 1종(csv 또는 shp) + prj + tfw로 완화, shp면 shx/dbf 동반 필수.
- UI: 확장자 단독 슬롯 매칭을 basename 그룹핑으로 바꿔 노선 PRJ와 지형 PRJ가
같은 슬롯을 다투지 않게 하고, 노선 슬롯이 파일 한 벌을 담아 함께 전송한다.
자체검증: tmp/tests/test_route_shapefile_input.py 9개 통과, tsc --noEmit 통과,
ruff check/format 통과. 전체 스위트 잔여 실패 11건은 HEAD 사본(git archive)에서
동일하게 재현되는 기존 실패다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_epsg_from_prj()가 WKT에 항상 있는 false_easting 때문에 EAST 분기가
무조건 걸려 모든 PRJ를 EPSG:5187로 판정했다(표준 WKT 6종 실측). 새 원청
자료의 5176(비표준 TOWGS84)·5179(AUTHORITY 없는 ESRI WKT)가 이 경로로
들어오면 최대 100km 어긋난다.
- common_util/common_util_crs.py 신설: 판별 사다리(pyproj DB 대조 →
AUTHORITY 태그 → 투영 파라미터 지문)와 변환 입력 정규화. 수평 성분에
TOWGS84가 박힌 PRJ는 EPSG로 갈아타지 않고 원문 WKT로 변환해 지역 보정을
보존한다. 수직 성분의 bound(KNGeoid)는 2D 변환에 무관하므로 무시.
- B04 get_epsg_from_prj: 죽은 문자열 분기 제거, 유틸 위임. 시그니처 불변 —
기존 COMPD_CS 프로젝트는 예전과 같은 EPSG:5187 문자열이 나온다.
- B03 _component_metadata: to_epsg 실패 시 사다리 라벨 보강,
normalize_crs_metadata: BoundCRS 벗김(TOWGS84 PRJ 수평 탐색 실패 수정).
검증: tmp/tests/test_common_util_crs.py 13건 포함 스위트 29 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 지적(2026-08-30): "결국 노선 위에 위치해야 한다".
관 자리를 정한 선(계획노선 CSV)과 화면에 그려지는 선(B05 최적 경로)은 같은 자리를
지나면서 연장이 다르다 — 실측 350.11m vs 354.83m, 전 구간 이격은 평균 0.03m·최대
1.12m뿐인데 길이가 4.7m 차이난다. pipe_points.json이 누가거리만 저장해서 읽는 쪽이
쥔 선에 따라 같은 값이 3.3~4.3m 미끄러졌고, 노선 지문도 늘 어긋나 B05는 저장된 관을
매번 통째로 버렸다("노선 지문이 달라 저장된 관 지점 4건을 쓰지 않습니다").
- PipePoint에 x·y를 두고 저장 시 채운다(save_pipe_points에 노선을 넘긴다).
- 지문이 달라도 좌표가 있으면 읽는 쪽 노선에 투영해 이월한다(project_pipe_points).
구간(start_m·end_m)은 기준점이 옮겨간 만큼 같이 민다 — 시설 치수는 불변.
- 좌표 없는 구 저장분만 종전대로 버린다.
- B05는 같은 로더(load_pipe_points_file)를 쓴다 — 두 화면의 판정 기준을 하나로.
검증: 프로젝트 5d18ebe3 실데이터 — 좌표 4/4 저장, B05 계획선으로 읽어 4건 전부 이월,
관에서 계획선까지 거리 최대 0.016m(종전 4건 전량 폐기). 누가거리는 +3.45~+4.29m
이동(연장 차이 그대로). pytest 11 passed(투영 이월·구 저장분 폐기 테스트 추가).