- 계획노선 사용 범위: 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>
- 자동설계 체인이 초기값 스냅샷에 `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>
증상: 파일 입력 직후 초기 계산값과 B05 [초기화] 결과가 다름.
원인
- 자동설계 체인이 5단계까지 전부 성공해야만 `save_initial_snapshot()` 호출.
중간에 깨지면 `initial_snapshot/` 미생성 → [초기화]가 복원 대신 재계산 폴백.
- 재계산 폴백의 지표면 기준이 체인과 다름 — 체인은 stage 1 확정값
(`get_surface_confirmation_params`), [초기화]는 config 기본값
(`surface_confirmation_defaults`). 실측: 프로젝트 stage1 `classification`/5m 대
config `csf`/1m.
- 체인이 깨져도 WF1 은 초기 분석 완료 메일을 그대로 발송.
수정 (2026-09-02 사용자 확정 — 부분 결과는 분석 안 됨과 다르지 않으므로 부분
스냅샷은 만들지 않음)
- `initial_design.failed` 마커 신설 — 프로젝트 루트(스냅샷 4트리 밖), 실패 사유 기록.
체인 진입 시 옛 마커 제거, 실패 5지점 + 스냅샷 저장 실패에서 기록.
- WF1 이 마커를 읽어 완료 메일 대신 `send_initial_design_failed_email()` 발송
(관리자 주소 `ADMIN_EMAIL`, 없으면 주소 없이 안내).
- B05 [초기화]: 스냅샷 있으면 복원, 실패 마커 있으면 409 `initial_design_failed`
로 거부(DELETE 앞에서 반환 — 데이터 무변경), 옛 프로젝트만 종전 재계산 폴백.
- 재계산 폴백의 지표면 기준을 `get_surface_confirmation_params()` 로 통일.
자체검증: `pytest tmp/tests/ -q` 366 passed / 14 skipped / 0 failed (+4).
공용 브라우저 실측 — 실패 프로젝트 [초기화] 409 응답·`routes` 행 무변경 확인.
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>
용화 예정노선으로 자동 체인을 돌리자 B05가 400으로 끊겼다.
세그먼트 1 (BP -> CP1) 경로 탐색 실패: 종단경사 한계(26%)로 통과 경로가 없습니다.
원인은 제어점 간격이다. Solver.py:394 는 제어점 쌍이 direct_link_max_m
(ROUTE_DIRECT_LINK_CELL_FACTOR 2.0 x ROUTE_GRID_RES_M 2.0 = 4.0m) 이하면 격자 탐색
없이 직결해 원청 계획노선을 그대로 보존하고, 경사·곡선 위반은 경고로만 남긴다.
샘플 CSV 350m / 135점 = 간격 2.6m -> 대부분 직결 -> 통과(곡선 위반 9건은 경고)
용화 예정 2136m / 121점 = 간격 17.6m -> 전부 탐색 -> 첫 구간 44.6%에서 실패
기존 프로젝트가 됐던 것은 제약을 만족해서가 아니라 탐색을 타지 않아서였다.
계획노선은 바꿀 수 없는 선이므로(사용자 확정), 같은 직선 위에 점을 더 찍어 간격만
좁힌다. densify_route()는 원래 정점을 모두 남기고 사이만 채운다 — 평면 형상도
연장도 끝점도 그대로다. 문턱과 정확히 같게 두면 부동소수 오차 한 번에 탐색으로
넘어가므로 ROUTE_PLANNED_DENSIFY_SAFETY(0.9)를 곱한다.
실측(용화, classification/dtm/smooth): 트림 후 63정점 -> 조밀화 323정점,
연장 1,070m 불변, 최대 간격 3.50m(문턱 4.0m), 시·종점 동일.
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>
원청 정식 계획노선이 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>
LAS 없이 설계한 프로젝트에서 WF1이 sheet_laplace로 자동 확정한 직후 체인이
config 기본값(csf/dtm)으로 B05 경로 계산을 걸어 404로 끊겼다.
ERROR 자동 설계 체인 중단(B05 경로 계산 실패): status=404
stage 1 스냅샷(get_surface_confirmation_params)을 읽어 실제 확정값을 쓴다.
B05 초기화가 부르는 경로도 같은 함수라 함께 고쳐진다.
검증: 프로젝트 5d18ebe3(LAS 없이 도엽등고선으로 생성)에서 체인 재실행 —
B05 route_id=112 연장 354.84m, 횡단 19개, 배수유역 관 4개·세부유역 8개까지
완주. 확정 선택값 {"source_filter": "sheet_laplace", "method": "dtm",
"smooth": true} 그대로 사용.
파일을 올리면 지표면 확정 직후 "노선 설계로 이동하세요"를 적고 그 뒤에 초기 설계
체인이 돈다. 그 사이 B05에 들어가 만지면 반쯤 계산된 값을 편집하게 되고, 그 편집이
섞인 채 초기값 스냅샷이 찍힌다. 계산이 끝날 때까지 준비 화면(B11)에서 기다리게 한다.
- initial_snapshot: 마커(initial_design.lock) 생성·해제·조회와 스냅샷 무효화,
편집 정본 제거를 한곳에 둔다. 마커는 스냅샷 4트리 밖(프로젝트 루트)이라 복원에
딸려 들어가지 않는다.
- 체인 두 곳(자동·재설계): 시작에 마커, finally에서 해제. 재설계 체인은 끝에 스냅샷을
무효화한다 — 사용자 입력을 유지한 채 재계산하므로 그 결과를 찍으면 가짜 초기값이
된다. 다음 [초기화]가 폴백을 타며 새 초기값을 만들고 그때 촬영한다.
- /surface/status: 마커가 있으면 in_progress + initial_design으로 답한다. stage 1이
COMPLETE면 진행률 파일을 무시하는 기존 분기를 건드리지 않으려고 별도 확인이다.
- 진입 게이트(routeAfterPreloadCheck)와 B11이 그 상태를 보고 기다린다. 상태 조회가
실패하면 막지 않고, B11 대기는 20분 상한을 둔다.
- [초기화] 폴백: 체인 전에 structures.json과 drainage/edits를 지운다. 남기면 구조물
측점이 사용자 편집분에서 다시 파생돼 초기값이 오염된다(2026-08-29 실측).
테스트 4건 추가. 249 passed·8 failed(기슭막이 이관 때 생긴 기존 실패, 변경 전 동일).
CLAUDE.md 5장(조작·데이터 흐름 정책)을 코드에 반영한다. 조작분은 세션에만 쌓고
영구저장은 [저장]·[확정]에서만 하며, [초기화]는 재계산이 아니라 초기값 복원이다.
자동저장 폐지
- B06 조정창 기준벽 구간값: 800ms 디바운스 PUT을 없애고 세션(b06:culvertopt)에
담는다. flushCulvertOptions()를 B06 [저장]·[확정]과 B05 [저장]이 부른다.
- B05 구조물: 조작 즉시 PUT + 서버 재조회로 화면을 덮어쓰던 것을 세션
(b05:structures) 적재로 바꾼다. 저장 전에도 고르고 지울 수 있도록 식별자를
crypto.randomUUID()로 미리 발급한다(서버는 빈 값일 때만 새로 발급).
- B05에서 만지고 B06에서 확정하는 경로를 위해 flushPendingStructures()를 공용화.
초기값 스냅샷
- common_util_initial_snapshot: 자동설계 체인 성공 직후 정본 파일 4트리와
routes+자식 4표를 initial_snapshot/에 뜬다. 이후 읽기 전용.
- reset_route_design: 스냅샷이 있으면 DELETE와 같은 트랜잭션에서 행을 되세우고
파일을 되돌린다. 없으면 종전 재계산 폴백. 응답에 restored를 더한다.
조작 응답
- 등고선 재적용·B06 진입 정합·[모두 적용]에서 전체 화면 오버레이 제거.
- 재계산이 design을 갈아끼울 때 extra_spans를 보존한다(다른 조작값과 동일).
테스트: tmp/tests/test_initial_snapshot.py 5건 추가. 245 passed·8 failed(기존
실패 — 기슭막이 이관 때 placement가 interval→point로 바뀐 것을 테스트 미반영).
계획선 1차 로직(design_pipe_anchored_profile)은 저장된 관 지점을 변화점으로
쓰는데, 자동 설계 체인이 그 파일을 만들어 주지 않았다. 그래서 초기 계획선이
2차 폴백(지반 추종 직선 분할)으로 산출돼 관 자리 틸팅·R이 통째로 빠지고,
B05/B06에 들어와도 배수유역도가 비어 있었다.
- _prepare_drainage_pipes_and_reprofile() 신설: 배수유역 분석 → 기본 관 지점
확정 저장 → 배관 정착 계획선으로 종횡단 재생성. 실패해도 체인을 멈추지 않는다.
- run_auto_design_chain: B05 경로 확정 다음, B06 확정 전에 호출.
- run_redesign_chain: 노선이 바뀌었으므로 refresh=True로 호출 — 저장된 배수유역
분석 응답은 노선 변경을 스스로 알지 못한다.
검증(테스트 프로젝트 fc86f247, 노선 350m): 관 4개(계류 1 + 간격 보충 3) 확정 후
종횡단 재생성 → PVI 6개(BP + 관 4자리 + EP)가 관 측점과 일치하고 각 자리에
R(1108.57/180.79/450.0/580.97m)이 붙는 것을 확인. 반영 전에는 관과 무관한
지반 추종 변화점이었다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
같은 업로드 한 번에 업로드 완료 메일과 지표면 분석 완료 메일이 두 통 도착했다.
사용자가 실제로 화면을 볼 수 있는 시점은 초기 설계(B04 전처리 + B05 종단 + B06 횡단)까지
끝난 때 하나뿐이므로 그 시점에 통합 메일 한 통만 보낸다.
- send_initial_analysis_complete_email 신설: 지표면 요약 + 노선 연장/측점 수 +
종단설계 화면 링크. 자동 체인 미실행/실패 시 전처리 요약만 싣고 안내 문구 전환
- run_auto_design_chain이 요약(route_id/length_m/cross_section_count) 반환
- WF1 서비스: 체인 실행 -> 통합 메일 순으로 재배치
- 업로드 직후 메일 발송 호출 제거(일반·청크 두 경로). 발송 함수는 프로젝트 생성 전
임시 보관함 안내용으로 남겨 두고 주석으로 용도 명시
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
자동 체인 상태 전이 분리:
- confirm_latest_route/confirm_sections에 mark_stage_complete 플래그 추가
- 자동 체인(신규·재확정)은 데이터만 확정하고 stage 2·3을 IN_PROGRESS로 남김
(스텝바 기존 노란 테두리 = 계산됨·확정 전 상태로 재사용)
- B06 [확정]이 stage 2+3 동시 완료 + 경로 상태 CONFIRMED 보강 후 B07 수량 이동
B05 좌측 패널 개편:
- 포인트 팔레트 + 임도 기준·옵션을 경로 계산 설정 한 컨테이너로 병합,
[최적 경로 계산] 버튼을 컨테이너 내부로 이동 (가끔 쓰는 무거운 재계산)
- 하단 액션 행 [초기화][임시저장][횡단 이동]으로 교체, 경로 단독 확정 폐지
- [임시저장] = 관로 + 계획선 델타 + 비정규 측점·상단측 저장(mark_stage_complete=false)
- [횡단 이동]/B06 [종단 이동] = 저장 없이 페이지 이동만 (B05·B06 캐시 공유 구조 유지)
- [초기화] = POST /route/reset 신설: 기존 경로 삭제 후 계획노선 CSV 기본값으로
자동 체인 재실행(파일입력 직후 상태 복원), 분석용 타임아웃 적용
B06 액션 행: [종단 이동][임시저장][확정] 3버튼 구성, locale 키 정비
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 12: run_redesign_chain — B04 재확정 시 사용자 입력(stage 2 params) 유지한 채
새 지표면 기준 B05 재계산·확정, 옛 측점별 설계·표준단면 설정을 chainage 매칭
이월 후 B06 확정. 경로 없으면 신규 자동 체인 폴백. B04 confirm에 백그라운드 결선
- 13: 구간 이동 글리프 ⇧⇩ → 속 찬 ⬆︎⬇︎(21×17·굵게), 규칙·비정규 측점 버튼
최소 간격 20px 캐스케이드 배치로 근접 측점 ▲▼ 겹침 해소
- 14: 유토곡선 손잡이를 표준 삼각형 손잡이(64×24, 바닥 중앙)로 통일, 캡션 제거,
'유토곡선 펼치기/접기' 툴팁
- 15: 유토곡선 스크롤러 세로 휠 → 가로 이동(종단 그래프와 동기)
Playwright 검증: 버튼 27개 최소 간격 20px, 글리프 ⬆︎ 21×17, 표준 손잡이 64×24,
휠 스크롤 0→400 종단 동기, 콘솔 오류 0. typecheck·ruff 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10. 재접속 현황·완료 표시·재업로드 경고:
- GET /projects/{id}/upload-overview 신설 — 완료 파일 목록(input_files 정본),
중단 청크 세션(진행률), 필수 파일·WF1 분석 완료 여부
- 진입 시 서버 정본으로 슬롯 카드 표시(serverUploaded), localStorage는 보조로 강등
- 파일 재선택 전에도 중단 세션 이어올리기 안내 배너
- 전체 완료 배지 + 완료 슬롯 재업로드 시 교체 확인 모달(승인 시에만 진행)
- 필수 슬롯 검증: 서버 업로드분 있으면 충족 — 단일 파일 교체 업로드 허용
9. 자동 설계 체인 연장 (B03_FileInput_Service_Chain.py):
- WF1 자동 확정 후 같은 백그라운드 태스크에서 ① 계획노선 CSV 기반 B05 기본 경로
계산(solve) ② 경로 확정(stage 2) ③ B06 기본 횡단 설계 확정(stage 3)까지 진행
- 수동 이력 보호: 프로젝트에 경로가 이미 있으면 건너뜀
- 단계별 실패 격리: 실패 단계에서 멈추고 로그·workflow 상태로만 기록
- AUTO_DESIGN_CHAIN_ENABLED config 플래그(기본 True)
typecheck·ruff·B03 unittest(7건) 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>