자동 체인 2.7 단계 추가 — 초기 폴리라인을 세운 뒤 노선을 다시 읽어
그것을 BP·경유점·EP 로 세우고 algorithm=as_planned 로 넘김.
전에는 폴리라인 파일만 만들고 노선은 원본 점군으로 풀었음. 그래서
종횡단 측점·유토곡선·3D 코리도가 곡선 없는 점군 위에 섰음.
실측(용화 3건): DB 노선 정점 331·323 vs 초기 폴리라인 145.
노선을 한 번이라도 편집한 프로젝트만 145 로 맞아 있었음.
자체검증 — 2.7 재판독이 145 정점을 돌려주고 BP·EP 좌표가
expected_route.csv 첫·끝 줄과 같음. pytest 409 passed.
계측 로그(_log_steps) 동봉 — 재확정 체인 단계별 경과시간용.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
링크 카드는 소유 측점의 벽을 빌려 그리는데, 빌릴 벽이 없는 카드에도 버튼이 떠 눌러도
아무 일이 없었음 — revetlink 에 detached 만 쌓였음(보조 창 실측: 0·20·40·60·80m 카드).
linkState 가 drawnWallKeys().length 도 함께 보게 함.
계획서 3-4 실화면 확인도 함께 마침(공용 브라우저, 용화):
- 벽이 있고 연동이 켜진 카드 820.00m 에서 [연동]을 끄자 revetx 에 820.00:outlet h=1.8 이
적힘 — 푸는 순간의 높이가 그 측점 값으로 굳음.
- 그 뒤 소유 측점 821.62m 높이를 4.2m 로 올려도 820.00m 은 1.8m 그대로 — 따로 놂.
※ 앞서 안 굳는 것으로 본 것은 측정 오류였음. 패널이 카드마다 하나씩 67개인데 첫 번째
패널만 봤고, 벽이 없는 카드를 골랐음(보조 창이 가려냄).
시험 409 통과·17 건너뜀, typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN 0-10. 그동안은 화면이 route/plan 을 처음 부를 때 만들어져, 화면을 안 열면 값이
없었음 — 초기값은 서버가 낸다는 0-4 원칙과 어긋났음.
- 파일입력 자동설계 체인이 예상노선 정본을 남긴 직후(2.6단계) 초기 폴리라인도 세움.
실패는 비치명적 — 없으면 화면이 처음 열릴 때 만들어짐.
- _ensure_planned_initial 이 **낡음도 본다** — 예상노선 파일이 더 나중에 쓰였으면 다시
만듦. 파일을 다시 올리면 예상노선이 새로 깔리는데 초기본이 옛 노선인 채로 남으면
노선 초기화가 옛 자리로 돌아갔음.
자체검증 — 예상노선 파일 시각을 최신으로 만든 뒤 route/plan 을 부르니 초기 폴리라인이
다시 만들어짐(노드 28 · 곡선 13 그대로). 시험 409 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
보조 창 지적 — [3D 업데이트]를 눌러도 브라우저가 만들기만 하고 PUT 이 없었음.
저장 시점이 B05->B06 이동뿐이라(2026-08-23 확정), 누른 뒤 새로고침하면 저장본이 낡은
채라 다음 사람이 또 만들어야 했음.
- 버튼 경로 끝에서 saveCorridorIfDirty 를 부름.
- saveCorridorIfDirty 가 PUT 성공 뒤 그 봉투를 브라우저 보관함에도 담음(옛 열쇠 자리는
치움). 그러지 않으면 다음 진입이 새 열쇠 주소로 16.9MB 를 한 번 더 받았음.
typecheck 통과. 실측 확인은 보조 창이 자기 프로젝트에서(저장본이 최신인 자리) 진행.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
보조 창 실측 — 노드를 옮겨 재계산하면 다단 구간값(extra_spans)이 0건이 됐음.
심어 둔 표식(85.18m 의 14/6/8)이 통째로 사라짐.
원인은 순서였음. 이월이 _prepare_drainage_pipes_and_reprofile(refresh=True) **앞**에서
돌았는데, 그 단계가 측점을 새 노선 기준으로 다시 만들며 방금 얹은 설계를 덮었음.
- 이월을 측점 재생성 **뒤**로 옮김. 옛 설계는 이미 old_designs 로 손에 있음.
- 행마다 쓰던 것을 merge_cross_section_designs 로 묶음 — 행 수와 무관하게 왕복 두 번
(원격 DB 왕복 약 12ms/질의).
같은 요청에서 확인된 통과분(보조 창) — 측점 수·끝 누가거리가 새 노선과 일치(67·1,097.4m
고정 -> 64·1,079.4m), 배수관 측점 0 -> 7~9, 「세그먼트 경로 탐색 실패」 로그 사라짐.
시험 409 통과·17 건너뜀, main import 스모크 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN 0-2 항목 9 / 0-3. 노선 편집 [확인]이 재확정 체인을 타는데 그 체인이 solve_route 로
BP·CP·EP 사이를 격자에서 다시 풀었음. 사용자가 노드를 조금만 비틀어도 탐색 제약에 걸려
체인이 통째로 멈췄음(보조 창 로그: 세그먼트 1 (BP -> CP1) 경로 탐색 실패 — 종단경사
한계 26% · 최소곡선반지름 12m · 회피지역 제약).
- B05_Profile_Engine_AsPlanned.solve_as_planned 신설 — 제어점 목록이 곧 노선이고 표고만
지표면 격자에서 뜸. 반환 꼴은 솔버와 같아 아래 단계가 그대로 이어짐.
종단기울기·곡선반지름 위반은 **세되 막지 않음**(사용자 확정: 자동 보정·차단 없이 경고만).
- run_route_design 에 algorithm=as_planned 갈래 추가. 스키마 검증에도 허용.
- 재확정 체인이 그 갈래를 씀 — 자동탐색은 이제 초기 업로드에서만 돎.
솔버와 지표 계산이 겹치지만 합치지 않음 — 솔버는 0-3 으로 접히는 코드라 리팩터링해
두 곳을 얽을 값어치가 없음.
시험 409 통과·17 건너뜀, main import 스모크 통과.
실화면 [확인] 재검증은 보조 창이 자기 프로젝트에서 진행.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
보조 창 실측 — [확인] 이 체인 중간에 400 으로 끊겨도 route/replan 은 success 를 돌려주고
계획노선 CSV 는 이미 새 노선으로 바뀐 뒤였음. 그 결과 배수유역·관은 새 노선, 종횡단은
옛 노선으로 어긋난 채 굳고(배수관 측점 9 -> 0), 다시 누를수록 어긋남이 쌓였음
(1,097.4 -> 1,079.4 -> 1,064.7m).
- run_redesign_chain 이 중단 사유를 문자열로 돌려줌(성공은 None). 중단 지점 넷 모두.
- replan 이 쓰기 전에 이전 수정본을 쥐고 있다가 실패하면 되돌림. 처음 고치는 경우였으면
파일을 지워 초기 폴리라인으로 돌아감.
- 실패는 409 + 사유로 응답 — 화면이 성공으로 보이던 것을 막음.
원인 자체(자동탐색이 사용자 노선을 다시 풀다 실패)는 별건으로 남음 — 로그 사유
「세그먼트 1 (BP -> CP1) 경로 탐색 실패: 종단경사 한계(26%)·최소곡선반지름(12m)·회피지역
제약으로 통과 경로가 없습니다」. 고친 노선을 그대로 받아 쓰는 입구가 필요함(PLAN 0-2 항목 9).
시험 409 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
보조 창이 노선 편집 [확인] 실측에서 둘을 잡음.
1. projects 에 design_speed_kph / terrain_type 컬럼이 없음(있는 것은 road_type 뿐).
매 요청마다 OperationalError(1054) 가 ERROR 로그로 남고 기본값 폴백으로 넘어갔음.
- 임도 종류는 projects.road_type, 설계속도·지형은 워크플로 stage 2 params 에서 읽음.
- 산식은 이미 있던 B05_Profile_Engine_Grade.legal_plan_radius_min_m 를 씀 —
내가 같은 표를 다시 짜 두었던 것을 지움(중복 제거).
2. 재확정 체인이 solve_route 400 으로 끊길 때 상태 코드만 남겨 원인을 못 짚었음.
본문(사유)까지 로그에 남김. 노선 편집 [확인]이 조용히 끊겨 배수유역·관은 새 노선으로
가고 종횡단만 옛 노선에 남는 어긋남이 굳었던 자리임.
recompute 의 상세 조회와 DB 두 건도 asyncio.gather 로 묶음(원격 DB 왕복 약 12ms/질의).
시험 409 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
손 안 댄 프로젝트를 열면 저장본 열쇠가 맞아 매번 16.9MB 를 받았고, 브라우저 보관이
모듈 Map 이라 새로고침 한 번에 날아가 또 받았음(보조 창 실측: 첫 로그인~B05 도달 29MB
중 16.9MB 가 이것). 해시 질의(0b30f5b0)는 낡았을 때만 막았지 맞을 때는 그대로였음.
- b_asset_cache 에 readCachedBytes / writeCachedBytes / purgeAssetsWithPrefix 추가.
앞 둘은 네트워크를 안 타는 순수 보관함 접근이고, 마지막은 주소 앞머리가 같은 옛
보관본을 지움 — 코리도는 주소에 열쇠가 박혀 정본이 바뀌면 새 주소가 되므로 17MB 짜리가
쌓이지 않게 담기 전에 치움.
- 코리도 조회가 보관함을 먼저 봄. 주소에 열쇠가 있어 정본이 바뀌면 저절로 다른 주소가
되므로 옛 보관본을 잘못 쓸 일이 없음.
자체검증(공용 브라우저) — 담기·읽기 왕복 정상(44바이트 왕복, 해시 보존), 열쇠만 다른 두 벌을
담은 뒤 앞머리 지우기로 둘 다 제거 확인. typecheck 통과.
※ 실제 16.9MB 절감은 저장본이 최신인 프로젝트에서 확인해야 함 — 이 프로젝트는 검증 중
계획고를 만져 저장본이 낡은 상태라 코리도 자체가 안 실림(__corridorSource null).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 지시(2026-09-06) — 노드를 제어해 계획노선을 고친다. 그동안 모달은 서버가 준
폴리라인 정점을 그대로 잡았는데, 거기에는 원호 위 점이 섞여 있어 편집 대상이 아님.
- 그려 보이는 선(폴리라인, 원호 포함)과 잡는 점(노드)을 나눔. 선은 plannedLine,
노드는 서버가 내려 준 nodes.
- 노드에 붙은 반지름·내각·법정 위반을 화면에 실음 — 위반 노드는 붉게, 상태줄에
곡선 수와 기준 R, 미달 개수.
- 노드를 옮기면 폴리라인은 낡은 값이므로 지우고 직선으로 미리 보임. 곡선은 [확인] 때
서버가 같은 R 규칙으로 다시 끼움(계산을 두 벌로 짜지 않음).
- API 타입에 nodes·min_radius_m·curve_count·violation_count 추가.
자체검증(공용 브라우저, 용화) — 모달이 노드 28개로 열리고 상태줄에
「노드 28개 · 초기 폴리라인 · 곡선 13곳(R 12m)」. 전에는 폴리라인 정점 142개를 잡았음.
typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Douglas-Peucker 허용오차는 절대 거리라 굴곡이 완만하고 길수록 통째로 삼켜짐.
4.5km 노선에서 노드 20개(간격 238m)만 남았음(보조 창 실측 5개 표본).
- 남은 노드 사이가 100m 를 넘으면 그 구간만 허용오차를 절반으로 낮춰 다시 뽑음
(최대 3겹: 4 -> 2 -> 1 -> 0.5m). 노선 길이로 허용오차를 바꾸면 짧고 급한 굴곡이 다시
뭉개지므로 벌어진 구간만 고름.
- shapely 대신 색인을 돌려주는 Douglas-Peucker 를 직접 둠 — 되뽑기가 원본 구간을 다시
꺼내야 해서 색인이 필요함. 새 점은 만들지 않음(노드는 언제나 원본 자리).
반지름을 원본 굴곡에서 재어 키우는 방법도 시험했으나 되돌림 — 직각 꺾임에서 R 70m ·
중앙종거 20m 가 나와 원본과 크게 어긋났음. 중앙종거 M = R(1/cos(delta/2) - 1) 이라
R 이 클수록 원호가 꺾임점에서 멀어짐. 법정 하한이 사용자가 지시한 기준값이자 원본에
가장 가까운 값이므로 그대로 둠. 곡선별 R 조정은 노드 편집 기능으로 분리(PLAN 0-10).
자체검증 — 1.1km 노선 3개: 노드 25 -> 28, 곡선 13, 위반 0, 최대 간격 82m.
4.5km 노선: 노드 20 -> 24. 그 노선의 최대 간격 1,300m 는 알고리즘 문제가 아니라 실제
직선 구간임을 확인(원본 점 간격 중앙 3.3m · 최대 3m 로 고르고, 내각 최소가 158°).
시험 2건 추가(완만한 굴곡이 살아나는지 · 되뽑기가 원본 점만 남기는지). 전체 409 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
계획서 2-2. 3D 픽·사이드 목록에는 structure-pick 기록이 있는데 종단 그래프 알약 경로에만
빠져 있었음 — 세션 키가 아예 안 써져 B06 이 잡을 값이 없었음(보조 창 추적).
- 구조물 브리지에 markChainage 추가 — 알약 id 의 누가거리를 구조물 정본·관 정본 양쪽에서
되읽음.
- Page 의 onStructureSelect 가 writeStructurePick 으로 그 자리를 남김(사이드 목록 경로와
같은 창구).
자체검증(공용 브라우저, 용화) — 알약 18개 중 셋을 눌러 세션 키가
찰쌓기 1 -> {at:40} · 찰쌓기 3 -> {at:80} · 옹벽 -> {at:120} 으로 써짐(전에는 null).
그 상태로 B06 진입 시 측점 6+0.0(120m) 카드가 선택된 채로 뜸. typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 확인(2026-09-06) — 예상노선은 폴리라인이 아니라 점 묶음이고 규칙 없는
폴리라인과도 맞지 않음. 그래서 계획노선은 원데이터를 복사해 폴리라인으로 바꾼 것이어야
하고 그것이 불변의 초기 데이터가 됨. 곡선 기준은 지식DB(별표2 I.2.다) 값을 씀.
- common_util_route_polyline.py 신설 — 점 묶음을 폴리라인으로.
① Douglas-Peucker 로 꺾임점(IP)만 남김. 예상노선은 격자 탐색이 낸 조밀한 점군이라
(용화: 1,097m 에 331점) 점마다 곡선을 끼우면 접선 자리가 1.5m 뿐이라 R 이 2~6m 로
뭉개짐. 허용오차는 격자 해상도(2m)의 두 배.
② 내각 155도 이상은 곡선 생략(별표2), 그 아래는 최소곡선반지름으로 원호를 끼움.
자리가 모자라면 반지름을 줄이되 막지 않고 위반으로 표시(사용자 확정: 경고만).
③ 점은 옮기지 않음 — 노드는 원본 자리에 그대로 두고 그 사이에 원호를 넣음.
- planned_route_initial_path 신설 — 세 벌 구조(예상노선 점 묶음 / 초기 폴리라인 /
수정본). load_design_route 읽는 순서에 초기 폴리라인을 예상노선보다 앞에 끼움.
- 최소곡선반지름은 임도 종류·설계속도·지형으로 고름
(FOREST_ROAD_PROFILE_CRITERIA[min_plan_radius_m], 못 읽으면 가장 완화된 조건).
- GET /route/plan 이 노드(반지름·내각·위반)까지 함께 돌려줌 — 노드만 옮기면 선이
저절로 규칙을 지키게 하는 것이 목적. POST /route/replan 도 받은 노드로 다시 폴리라인화.
자체검증(용화, 실화면 API) — 예상노선 331점 -> 편집 노드 25개 + 곡선 13곳
(전부 R 12m = 설계속도 20·특수지형 법정 하한, 위반 0) -> 계획노선 정점 142개. 응답 180ms.
단순화 전에는 곡선 15곳이 R 2.75~6.51m 로 전부 위반이었음.
시험 tmp/tests/test_route_polyline.py 7건(원호가 접선과 맞물리는지 좌표로 대조).
전체 407 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
표식(측점 수 + 누계)만으로는 부족했음 — 응답을 반영해 다시 그린 뒤 누계가 아주 조금
달라지는 경로가 있어 편집이 멈춘 뒤에도 2~3바퀴가 더 돌았음(보조 창 10초 관찰:
2.8~6.3초에 걸쳐 3건).
onReady 로 다시 그리는 동안 applying 을 세워 그 그리기가 거는 schedule 을 무시함.
자체검증(공용 브라우저, 접은 채 계획고 6회 편집 후 10초 관찰 3회) — 3/3/1건 -> 2/1/1건.
편집하는 동안은 종전대로 0건(400ms 안정화 유지). typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- schedule 호출이 draw 의 접힘 가드 뒤에 있어 패널을 접어 두면 한 번도 안 불렸음.
그래서 「편집이 멈추면 조용히 받아 둔다」가 성립하지 않고 펼친 뒤에야 첫 요청이 나갔음
(보조 창 실측: 편집 6회에 요청 0건). 접힘 여부와 무관하게 지나는 emitSummary 로 옮김 —
거기서 이미 고른 결과를 쓰므로 계산 비용 0.
- 응답이 오면 곡선을 다시 그리고, 그리기가 다시 schedule 을 불러 요청이 끝없이
되돌아왔음. 입력 표식(측점 수 + 마지막·중간 누계)을 두어 같은 값이면 안 보냄.
자체검증(공용 브라우저, 패널 접은 채 계획고 6회 편집) — 요청 1건, 그 뒤 4초 동안
추가 0건. 고치기 전에는 편집 중 0건 + 펼친 뒤 1건 + 되먹임 1건이었음.
시험 400 통과·17 건너뜀, typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
행 높이를 재는 1차와 실제 그리는 2차가 같은 측점의 crossPlotMetrics 를 각각 계산했음.
측점 67곳 기준 3.2ms/장이라 진입에서 draw 자기 시간 213ms 의 대부분이었음
(보조 창 CPU 프로파일).
- crossPlotMetrics 를 crossPlotBase(강제 높이 전) + crossPlotFromBase(높이만 얹기)로
가름. 기존 시그니처는 둘을 잇는 감싸개로 유지.
- Section_View 1차에서 base 를 만들어 2차 카드에 그대로 넘김.
- 1차 반폭도 카드가 실제로 쓰는 값(effectiveCardHalfWidth, 자동 줌아웃 포함)으로 맞춤 —
전에는 1차가 원래 반폭으로 재 카드·행 높이 기준이 어긋났음.
- 쓰이지 않게 된 crossCardNaturalHeight 와 그 재수출 삭제.
자체검증(공용 브라우저) — 카드 67장·SVG 67개 그대로, 같은 행 카드 높이 통일 유지
(rowsUniform true), 높이 범위 292~1,119px. 시험 400 통과·17 건너뜀, typecheck 통과.
효과 수치는 보조 창의 격리된 프로젝트에서 3회 중앙값으로 재기로 함(내 창은 편차가 큼).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
배분·운반거리 산식(_balance 523 + _settle 235 = 758줄)을 브라우저 번들에서 빼기 위한
배선. 계산은 여전히 한 벌 — 서버가 같은 TS 를 Node 로 돈다(CLAUDE.md 5장).
- POST /projects/{id}/sections/{route_id}/haul-plan 신설
(B06_Section_Router_HaulPlan.py). 브라우저가 낸 누가토량 결과를 받아 배분만 돌려줌.
화면이 쓰는 꼴 그대로 내보내 그리기 코드를 안 건드림. 표시 전용 — 정본은 저장 때 따로.
- B05_Profile_Api_HaulPlan.ts 신설 — 편집이 멈추면(400ms) 조용히 받아 두는 선반입기.
늦게 온 응답은 버림(최신 요청만 채택). 못 받아도 곡선 자체는 그대로 보임.
- common_util_mass_haul.massHaulPayload 가 배분을 값으로 물던 것을 끊음(extra 인자).
이 커플링 때문에 화면에서 안 불러도 번들에 남았음.
- 저장 경로는 곡선도 배분도 안 만듦 — 서버가 정본을 내므로 balloon 위치만 보냄.
- 죽은 파일 B06_Section_UI_Section_View_MassHaul.ts(236줄) 삭제.
자체검증(공용 브라우저, 용화 route 149) — 엔드포인트 200 · 196ms.
서버 배분과 브라우저 computeHaulPlan 결과가 JSON 문자열까지 동일(blocks 1 · steps 2 ·
spoil/borrow/hauled/transferred/fill_total 전부 일치). 시험 400 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
카드마다 requestAnimationFrame(reflow) 을 걸어 한 프레임에 67번 돌았고, 매번
쓰기 뒤 읽기라 강제 레이아웃이 67회 났음. 한 번의 강제 레이아웃이 그때까지 들어간
카드 전부를 다시 재므로 뒤로 갈수록 비쌌음(보조 창 CPU 프로파일: 자기 시간 1위).
- reflow 를 reset(쓰기) / measure(읽기) / apply(쓰기) 세 토막으로 가름.
- 모듈 단위 scheduleReflow 가 카드를 모아 한 프레임에서 전부 reset -> 전부 measure ->
전부 apply 순으로 돌림. 강제 레이아웃 프레임당 1회.
- ResizeObserver 는 창 크기 변경 때 그 카드만 큐에 다시 넣음(첫 호출 건너뛰기 유지).
자체검증(공용 브라우저) — B06 진입 2,330~2,864ms -> 1,994/2,088ms,
긴 작업 합 1,853 -> 1,604/1,728ms. 동작 확인: 카드를 200px 로 좁히면 버튼 3개가
... 패널로 옮겨지고 되돌리면 복귀(정상 폭에서는 scrollWidth = clientWidth = 937 로
넘침 없음). typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
route/confirm 이 측점마다 종단 정본을 다시 열고(13.8ms) 행마다 DB 에 썼음(24.5ms).
건당 38.3ms 라 30건이면 1.15초, 67건이면 2.57초로 측점 수에 선형으로 늘었음
(보조 창 서버 내부 측정).
- 다시 계산할 측점을 먼저 고른 뒤 asyncio.to_thread 를 **한 번만** 돌림. 그 안에서
종단 정본과 포장 제안표·세월교 하강표를 한 번 읽어 측점마다 돌려 씀.
- read_cross_design_inputs 에 preloaded 인자 추가(종단 경로·내용·포장 제안표).
경로 검증은 resolve_longitudinal_path 로 떼어 재사용.
- 쓰기는 merge_cross_section_designs 한 문장.
자체검증(공용 브라우저 [저장] 3회) — route/confirm 470~834ms -> 324/351/356ms.
버튼 전체 대기 2,624~3,415ms -> 1,826/2,766/2,979ms.
(오늘 누적: 4,137ms -> 1,826~2,979ms) 시험 400 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B05 진입이 간헐적으로 13~14초 멎던 원인을 CDP 프로파일로 잡음. 13,537ms 중
getVertexPosition 3,267 + intersectTriangle 3,178 + checkGeometryIntersection 3,012
+ _computeIntersections 2,198 = 11.6초가 three.js 레이캐스팅이었음.
- terrainElevation 이 점마다 Raycaster 로 지형 높이를 찾고 있었음(한 번 쏠 때마다
삼각형 전수 훑기). 마커·측점선이 점마다 부르는 자리라 곱해짐. 이미 있던
TerrainHeightIndex(격자 색인)로 돌리고, 색인도 지형과 한 벌로 보관(빌드 O(삼각형)).
같은 교훈을 2026-08-23 비탈 투영에서 겪어 색인을 만들어 뒀는데 이 함수만 옛 길이었음.
- B06 카드 버튼줄 reflow 의 레이아웃 스래싱 제거 — 한 칸 옮길 때마다 scrollWidth 를
다시 읽어 매번 강제 레이아웃을 냈음(카드 67장마다). 읽기/쓰기를 갈라 폭을 한 번만
재고 옮길 개수를 계산. ResizeObserver 첫 호출도 건너뜀(rAF 와 겹쳐 두 번 돌았음).
- get_section_context·get_section_detail 의 순차 읽기를 asyncio.gather 로 묶음
(원격 DB 왕복 약 12ms/질의).
자체검증(공용 브라우저 4왕복) — B05 진입 14,708/1,060/14,841 -> 1,269/806/726/746ms.
급등 사라짐. B06 진입 2,884 -> 2,330~2,864ms(긴 작업 합 2,313 -> 1,853).
시험 400 통과·17 건너뜀, typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
측점마다 SELECT+UPDATE 두 왕복을 냈고 DB 가 원격이라 왕복 하나가 약 12ms.
22행이면 670ms 이고 측점 수에 선형으로 늘었음(보조 창 서버 내부 측정).
- B06_Section_Repository_Bulk.merge_cross_section_designs 신설 — 노선 측점을 한 번에
읽고, 파이썬에서 chainage 를 맞춰 JSON 을 합친 뒤
`UPDATE ... SET data = CASE id ...` 한 문장으로 되돌려 씀(없는 행은 다중 INSERT).
행 수와 무관하게 왕복 두 번. Repository 가 685줄이라 파일을 나눔(700줄 한계).
- 부르는 자리 셋을 묶음으로 바꿈 — _apply_section_edits 의 기본설계·측점 patch 두 루프,
_recompute 의 보정·면적 두 루프.
자체검증(공용 브라우저 [저장] 3회) — sections/save 3,593ms -> 2,205 / 1,870 / 2,547ms.
버튼 전체 대기 4,137ms -> 2,624~3,415ms. 진행 표시는 2~3ms 만에 뜸.
시험 tmp/tests/test_b06_bulk_designs.py 5건(같은 JSON 결과·왕복 두 번·1cm 허용오차·
행 없을 때 INSERT·빈 목록은 왕복 0). 전체 400 통과·17 건너뜀.
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>
인증이 걸린 모든 요청이 지나는 자리인데 요청마다 UPDATE + commit 이 나갔음.
DB 가 원격이라 그 둘이 곧 왕복 20ms 임.
실측 — verify_session 34.4ms = SELECT 10.8 + UPDATE 10.4 + commit 9.5.
쓰기를 1분 간격으로 낮춘 뒤 13.6ms(최소 9.4).
last_activity_at 은 유휴 판정에만 쓰는 값이고 그 판정이 4시간 단위라
초 단위 정확도가 필요 없음. 간격은 SESSION_ACTIVITY_WRITE_INTERVAL_SECONDS
(기본 60, .env 로 조정).
화면 한 번 여는 데 API 가 약 180번 나가므로 그만큼 곱해짐.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B05 는 해시가 바뀔 때마다 renderB05Route 로 통째로 다시 조립되어 뷰어 상태가 비워짐.
그래서 8MB 지표면을 진입할 때마다 다시 읽고 다시 파싱했고, 그것이 진입을 잡는
단일 동기 블록 14.4초였음(공용 브라우저 3왕복 실측: 14,708 / 1,060 / 14,841ms).
- 파싱해 둔 지형을 모듈 단위 cachedTerrain 에 한 벌 보관 — 같은 모델이면 새 장면에
그대로 붙임. 다른 모델을 부르면 옛것을 버려 GPU 버퍼가 안 쌓임.
- 장면에서 뗄 때 보관본은 disposeObject 하지 않음.
- window.__surfaceTiming 디버그 훅 추가 — 단계별 시간(fetch·parse·fit·contours).
자체검증(공용 브라우저 3왕복) — B05 진입 683 / 474 / 639ms (전 14,708 / 1,060 / 14,841).
surfaceTiming: reuse 0ms · fit+markers 35ms · contours 183ms · total 222ms.
B06 진입은 2.4~2.7초로 변화 없음. 시험 395 통과·17 건너뜀, typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
계산 결과는 화면에 나가도 된다는 방침이라, 남는 위험은 입력을 바꿔가며 출력을
긁어 모으는 것임. 사람이 화면을 쓰는 속도에는 한계가 있음 — 화면 한 번 여는 데
API 가 약 180회 나가므로 한 시간에 2만 회를 넘으면 사람이 아니라고 봄.
- 막지 않음. 고객 화면을 끊을 위험이 있고 어디서 끊을지는 실제 사용 기록을 본 뒤
정할 일임. 지금은 시스템 로그(보관 1년)에 RATE_ANOMALY 한 줄만 남김.
- 세는 값은 메모리(세션·시간대별)라 요청당 DB 비용 0. 상한을 넘은 그 순간에만
세션 주인을 한 번 조회해 기록하고, 같은 시간대에는 다시 안 남김.
- 지난 시간대 기록은 버려 사전이 무한정 자라지 않게 함.
- 상한은 API_CALL_HOURLY_LIMIT(기본 20000, .env 로 조정).
빌드 껍데기는 이미 단단해 손대지 않음 — 실측 확인: 배포 산출물에 소스맵 0개,
식별자 뭉개짐(import{$ as e,C as t,...}), computeCrossDesign 등 원본 함수명
검색 결과 0건. 난독화 도구 추가는 얻는 것 대비 비용이 큼.
시험 tmp/tests/test_call_volume_watch.py 5건. 전체 484 통과.
실서버 확인 — 정상 사용 흐름에서 감시 로그 0건.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
등고선 도엽 36.8MB 를 매 요청 level 9 로 눌러 9.4초가 걸렸음(실측).
level 1 은 줄어드는 양이 거의 같고(66MB 기준 24.0 vs 23.9MB) 시간만 1/3.
실측(8001, IPv4) — 등고선 도엽 739ms · 전송 11.3MB(전 65.9MB 무압축),
종횡단 상세 485ms · 전송 0.33MB(전 1.56MB), 배수유역 전송 22KB(전 151KB).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 저녁 사용자 확정. 이유는 속도가 아니라 보안 — 저장 경로가 브라우저 계산이면
유토 배분·운반거리 코드(_balance 523 + _settle 235 = 758줄)가 번들에 남아야 해서
화면에서 안 그려도 뺄 수 없음. 서버가 정본을 내면 그 몫이 번들에서 빠짐.
대가는 저장 대기 850ms -> 약 980ms(Node 131ms) — 기다리는 조작이라 허용.
- 저장·확정이 편집을 커밋한 **뒤** recompute_server_side 를 부름(전에는 편집 앞에서
Node 없이 보정만). 바뀐 벽·측점이 면적·유토곡선에 실림.
- run_node 갈래 제거 — 이제 부르는 자리가 둘 다 Node 를 돎(체인·저장).
쓰이지 않게 된 enforce_stored_designs 래퍼 삭제.
- 코리도 열쇠 질의: hash 를 isinstance 로 좁힘. 파이썬에서 곧바로 부르면 Query 기본값
객체가 들어오는데 truthy 라 열쇠 없는 호출이 통째로 stale 로 떨어졌음(보조 창 지적).
시험 395 통과·17 건너뜀.
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>
`B06_Section_Router_Confirm` 이 `_compute_default_designs` 를 `B06_Section_Router`
에서 들여왔는데, 커밋 1d4568b9 가 그 재수출을 지우면서 서버가 아예 안 떴음.
원본 모듈 `B06_Section_Router_Design.compute_default_designs` 에서 곧바로 들여옴.
시험 390건이 이 경로를 안 타서 못 잡았으므로 tmp/tests/test_app_imports.py 추가 —
`main` 을 들여와 라우터 27개가 전부 import 되는지 본다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B05 에 들어가는 것만으로 코리도 저장본 18.6MB 를 통째로 받고, 열쇠(해시)에 종횡단
내용이 섞여 있어 계획고를 한 번이라도 만졌으면 받아본 뒤 버리고 브라우저가 다시
만들었음(실측 화면 전환 16.7초). 3D 는 이미 [3D 업데이트] 수동인데 진입 경로만 자동.
- 서버: GET corridor 에 `?hash=` 를 받아 저장본 머리 4KB 에서 열쇠만 떼어 맞춰 봄.
어긋나면 파일 대신 `{status:stale}` 만 냄. 맞으면 종전대로 파일 — 왕복은 한 번.
- 화면: 진입은 `loadCorridorIfFresh` 로 **저장본이 맞을 때만** 3D 에 올림.
낡았으면 받지도 만들지도 않고 [3D 업데이트] 대기 표시만 켬.
- `ensureCorridor` 에 `rebuild` 인자 추가 — [3D 업데이트] 버튼 경로는 종전대로 재생성.
시험 tmp/tests/test_b05_corridor_hash_probe.py 3건.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
perf(B04): 배수유역 세부 계산의 헛일 제거 — 282→154ms
DB 가 원격(dsm.chemifactory.com)이라 질의 하나가 곧 왕복 12ms 임을 실측.
순차 6건 130ms 중 대부분이 왕복 대기였음.
- context: 서로 기다릴 이유가 없는 질의를 두 묶음으로 asyncio.gather
(묶음마다 자기 커넥션 — 풀 최대 20 이라 여유). 계획노선 읽기·지표면
샘플러 열기도 같이 보냄.
- 격자 산출물(npz 3.1MB): 관을 옮길 때마다 다시 읽던 것을 파일 자국
(수정시각·크기) 열쇠로 재사용. 분석이 다시 돌면 자국이 바뀌어 저절로 새로 읽음.
자체검증 — 용화 프로젝트(관 11·유역 11) 5회 중앙 282→154ms.
같은 입력에 응답 전체가 한 글자도 안 달라짐(JSON 정렬 비교).
전체 테스트 390 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
관 추가·이동·삭제가 B05 패널 메모리에만 있어, B06 으로 넘어가 [저장]·[확정]하면 그
편집이 사라졌음(대응표 조사에서 드러남).
- 등록표에 `pipes` 초안(프로젝트 범위)을 되살림
- 배치가 바뀌는 순간 목록을 초안에 담고, 내보내기는 `flushPendingPipes` 한 창구로 모음
- B05 [임시저장]·B06 [저장]·[확정] 모두 이 창구를 지남 (자동저장 없음)
검증: 초안으로 관 11 → 10개 저장·초안 비움, 되돌려 11개 복구. 테스트 390 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>