- B01 프로젝트 수정: 과업책임자·분야별책임자·설계자를 회사 구성원 목록(select)에서
지정. 회사 로고·설계자 서명 칸은 누르면 등록 목록 모달(선택·삭제·신규 추가 업로드).
- 백엔드: `company_assets`(013) CRUD 라우트 신설 — 목록·업로드(Form+File, 2MB·확장자
검사)·수정·소프트 삭제·파일 서빙. `_scope_company` 로 SYSTEM_ADMIN 만 타 회사 지정,
`_check_project_refs` 로 담당자·자산이 같은 회사·같은 종류인지 검사(400).
- projects 조회·수정에 pm/field_lead/designer/logo_asset/signature_asset 5칸 합류.
중복 3곳을 `_project_rows` 로 묶어 700줄 제한 유지.
- B07 표제란: 로고·서명 출처를 `companies.logo_path`/`users.signature_path` 에서
프로젝트가 고른 `company_assets` 로 전환(삭제된 자산은 그림째 제외).
- 검증: pytest 143 통과(신규 4), tsc·ruff·prettier 통과. 공용 브라우저 사용자 경로
(업로드→선택→저장→재열기) 및 종단도 API 표제란 글자 4건·그림 2건 수치 확인.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 확정(2026-09-02): 설계일자는 확정일 자동이 아니라 **사용자가 넣는다**. 도면번호는
Claude 판단으로 목록 순번을 쓴다.
설계일자
- `013_company_assets.sql` 로 `projects.design_date DATE` 추가(같은 마이그레이션에
로고·서명 공유 테이블 자리도 함께 만들었음 — 배선은 다음 커밋).
- B01 프로젝트 수정 모달에 날짜 칸. 공용 `createInputField` 가 `type="date"` 를 받게 넓힘.
- 표제란에는 도면 표기 관행대로 `2026. 09. 02.` 꼴로 적는다.
도면번호
- 단건 조회는 목록 순서를 모른다 — 알려면 횡단 장 계획을 다시 계산해야 하고 도면을 열
때마다 그러면 비싸다. **목록이 매긴 번호를 manifest 에 적어 두고** 단건 조회가 읽는다.
목록은 화면 진입 때 늘 먼저 뜨므로 성립하고, 목록이 바뀌면 다음 조회에서 다시 적힌다.
- `add_title_fields()` 신설 — 도면을 읽는 도중에 아는 값을 문맥에 얹는 창구.
검증: `pytest tmp/tests/ -q` 139 passed / 0 failed, 루트 `tsc --noEmit` 통과.
실사용자 경로(5174, wdw) — 대시보드 [수정] 모달 라벨 10개(설계일자 칸 추가 확인) →
`2026-09-02` 입력 → [확인] → 종단면도 표제란에 `2026. 09. 02.`.
도면번호는 목록 13건 기준 종단 1 · 유역도 6 · 용지도 13 이 도각에 그대로 실림
(표지는 도각을 두르지 않아 번호 칸이 없다).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
표제란에 남아 있던 빈칸 중 연도·기번과 사업량은 프로그램이 지어낼 수 없는 값이라
입력 자리를 만든다. 값의 출처를 정하지 않고 받을 칸만 세운다.
DB (`012_title_block_inputs.sql`, ADD COLUMN·NULL 허용)
- `projects.project_number` — 표지 연도·기번(실무문서 폴더명 관행:
`2024년 간선임도(기번3-울진.대흥)`)
- `projects.work_amount` — 표지 사업량. 단위·표기가 사업 종류마다 달라 자유 문자열.
B01 프로젝트 수정
- 수정 모달에 시행청·연도기번·사업량 3칸 추가. 011 로 만든 시행청도 여기서 처음 입력
가능해짐(그전에는 스크립트로만 넣었음).
- 목록·단건 조회 4곳과 UPDATE 에 세 칸을 함께 실음. USER 권한은 다른 칸과 같이 잠금.
B07
- `_title_block_fields` 가 연도기번·사업량도 실어 보냄. 비어 있으면 종전대로 빈칸.
검증: `pytest tmp/tests/ -q` 139 passed / 0 failed(표지 값 1건 신규), 루트 `tsc --noEmit`
통과. 실사용자 경로 실측(5174) — 대시보드 wdw 행 [수정] 클릭 → 모달 라벨 9개에 새 3칸
확인 → 연도기번·사업량 입력 → [확인] 저장 → 표지 도면 Text 7개 **빈칸 0 · `{{` 잔존 0**,
화면에도 연도기번·`wdw 설계도`·위치·사업량·시행청이 모두 그려짐.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
빈 도면 7종이 도각만 두르는데도 경고를 냈음 — `build_blank_drawing()` 이 콘텐츠
bbox 로 작도 영역(`_A1_INNER`, 770×520mm)을 그대로 넘겼고, 검사 기준은 여백을 뺀
수용 한도(739×499mm)라 담을 내용이 없는데도 넘친 것으로 잡혔음.
- `usable_bbox()` 신설 — 작도 영역 한가운데의 수용 한도 크기 bbox. 중심이 같아
도각 배치(이동량 0)는 그대로.
- 한도와 **같은** 크기는 넘친 것이 아니므로 비교에 부동소수 여유(1e-6)를 둠.
한도를 그대로 넘기면 마지막 자리 오차로 경고가 났음.
검증: `pytest tmp/tests/ -q` 138 passed / 0 failed(경고 없음을 잠그는 1건 신규).
백엔드 재시작 뒤 화면 실측(5174, wdw): 빈 도면 7종 전부 200, 새 서버 로그의
"작도 영역을 넘습니다" **0건**.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
축척은 지어내는 값이 아니라 이미 정해져 있던 것 — `config_system` 의 도면별 축척
상수를 그대로 표제란에 싣는다. 도각이 `A1 = 1 :` 를 이미 찍으므로 **분모만** 낸다.
- `scale_fields()` 신설 — A3 는 A1 도면을 절반으로 뽑는 종이라 분모가 2배. 가로·세로가
다른 도면(종단·토적)은 이름표를 붙여 함께 적는다.
- 종단면도 `1000(H) · 200(V)` / 횡단면도 `100` / 유역도 `6000` / 토적도 `2000(H)`.
A3 는 각각 2배. 담을 내용이 없는 빈 도면·표지는 축척이 없어 빈칸 그대로.
검증: `pytest tmp/tests/ -q` 135 passed / 0 failed(축척 2건 신규 — 상수 유래·A3 2배,
호출부가 넘긴 값이 도각에 실림). 백엔드 재시작 뒤 화면 실측(5174, wdw): 종단
`1000(H) · 200(V)`/`2000(H) · 400(V)`, 횡단 `100`/`200`, 유역도 `6000`/`12000`,
토적도 `2000(H)`/`4000(H)`, 표준도 빈칸.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
도각의 로고·서명 자리에 흰 사각형이 남던 것 — `ImageEntity.draw()` 가 폴리곤 네 변을
선택·강조 여부와 무관하게 늘 그렸음. 출력·내보내기가 같은 `draw()` 를 다른
DrawController 로 타므로 화면만의 문제가 아니라 산출물에도 실렸음.
- 도각층만 특례로 두지 않고 `ImageEntity` 전체에서 뺌. 일반 CAD 관행대로 **집었을
때만**(강조·선택) 테두리를 보임 — 앞으로 붙일 그림(현황사진 등)에도 같은 규칙.
- 자리 표시가 필요하면 도각 편집 화면에서 집으면 보임.
검증: `ImageEntity.test.ts` 신규 2건 — 안 집은 그림은 `drawLine` 0회·`drawImage` 1회,
집은 그림은 4회·1회. `npx vitest run` 89 passed / 0 failed, check-types·build 통과.
화면 실측(5174, wdw 표준도): 두 슬롯 사각형 안 흰 픽셀(RGB>200) **0**,
그림 픽셀은 그대로(로고 56 · 서명 106).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 지시(2026-09-02) — 로고·서명을 임의로 만들어 표제란에 넣고, 위치·크기는
도각을 고치면 따라오게 하며, 시행청·과업책임자 등은 DB를 넓혀 채울 것.
도각(표제란)
- `00_template_A1.json` 에 그림 자리 2개 추가 — 회사 로고는 용역회사 칸 왼쪽
(316~348 × 20~36mm), 설계자 서명은 설계 칸 아래(638~680 × 17.5~25.5mm).
자리·크기가 **템플릿 좌표로만** 정해지므로 도각을 고치면 그대로 따라옴.
- `_fill_placeholders` 가 그림 자리(`imageData`)의 `{{키}}` 도 채움. 값을 못 구하면
빈 문자열을 남기지 않고 **엔티티째 제거** — 빈 값은 CAD 가 깨진 그림으로 그림.
- `_transform_entity` 가 `points` 배열도 옮김. 그림(로고·서명)과 띠(Hatch)가 이 키를
쓰는데 여태 변환 대상이 아니라 도각을 옮기면 제자리에 남았음.
DB (`011_title_block.sql`, 전부 ADD COLUMN·NULL 허용)
- `projects` — `client_org`(시행청), `pm_user_id`·`field_lead_user_id`·`designer_user_id`
- `companies.logo_path` · `users.signature_path` — 그림은 파일로 두고 경로만 담음
(기존 `storage_path`·`input_files` 와 같은 방식)
- FK 는 걸지 않음 — 사유를 파일 머리말에 적음(소프트 삭제·LEFT JOIN·4환경 공유).
배선
- `_title_block_fields` 가 시행청·과업책임자·분야별책임자·설계자(배정 없으면 소유자)와
로고·서명 data URL 까지 실어 보냄.
- `read_stored_asset()` 신설 — `storage/` 기준 상대 경로의 **파일**을 읽음.
`resolve_stored_project_path()` 는 폴더를 만드는 프로젝트 루트용이라 파일에 못 씀.
CAD 결함 1건 (이번 작업에서 드러남)
- `screenCanvas.drawController.drawImage` 가 `(width, height)` 를 좌표처럼 변환해
화면 오프셋과 y 뒤집기가 섞여 들어갔음 — 그림이 제 자리를 벗어나고 비율이 무너짐.
크기는 배율만 곱하고 자리는 세계 중심으로 잡도록 고침(SVG 컨트롤러와 같은 방식).
- 검증 창구 `__aisloCad.screen(x, y)` 추가 — 세계→화면 좌표. 그림·글자가 제 자리에
그려졌는지 픽셀로 판정할 때 씀.
검증: `pytest tmp/tests/ -q` 135 passed / 0 failed(그림 자리 3건 신규),
`npx vitest run` 87 passed / 0 failed, `check-types`·`build` 통과.
화면 실측(5174, wdw): 표준도 API Text 24개 `{{` 잔존 0, Image 2개가 도각 좌표
(316,20)-(348,36)·(638,18)-(680,26)에 실림. 캔버스 픽셀 판정 — 두 자리 모두 배경색
외 픽셀이 그려짐(로고 88px·서명 64px), 로고 파랑(20,70,140)이 슬롯 안에서만 검출.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
교차검증에서 표지만 빈칸으로 나갔음 — 문맥값을 합치는 자리가 `frame_entities` 안에
있어, 도각을 두르지 않는 표지 경로(`build_cover_drawing`)가 비껴갔음.
- 합치는 자리를 치환 함수 `_fill_placeholders` 한 곳으로 옮김. 도각을 쓰든 안 쓰든
`{{키}}` 를 치환하는 모든 경로가 같은 값을 받음 — 앞으로 생길 경로도 자동으로 닿음.
- 표지 docstring 의 "값 공급은 다음 판 몫" 문구 정정.
검증: `tmp/tests/test_b07_title_block_fields.py` 에 표지 케이스 추가(공사명·위치가
표지에 실리고 `{{` 잔존 0). `pytest tmp/tests/ -q` 132 passed / 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
표제란(도각) 값 공급
- `use_title_fields()` 문맥 변수 신설 — 회사 도각 폴더와 같은 방식이라 작도 엔진
6개의 서명을 고치지 않음. `frame_entities` 가 문맥값 위에 호출부 값(도면명)을 얹음.
- 라우터가 도면 한 건을 그리기 전에 DB 값을 세움: 공사명·위치(projects),
용역회사(companies), 설계자(users). 못 채운 자리는 빈칸 — 도각 원본의 남의 값이
도면으로 나가지 않음.
- 미공급 항목과 사유를 `_title_block_fields` 주석에 명시: 시행청·과업책임자·
분야별책임자는 DB 칸 부재(B02 마이그레이션 대기), 축척·사업량·연도기번은 임의 수치
금지, 설계일자는 채울 시점 정의 미결.
CAD 회귀 6건 (87건 중 81 passed → 87 passed)
- `window` 부재 5건: `notifyWindow()` 한 곳으로 모으고 `window` 없는 Node 시험에서는
통지를 건너뜀. `triggerReactUpdate` 가 이미 같은 이유로 시험을 건너뛰던 것과 같은 결.
- `find-closest-entity`: 목의 호가 90°까지만 돌아 클릭점에서 147px 떨어져 있었음 —
시험 의도(호가 가장 가깝다)대로 3/4바퀴로 되돌림.
- 선색 기대값 4건: 앱 기본 선색이 흰 종이 배경에 맞춰 검정으로 바뀐 뒤 상류 기대값
`#fff` 가 남아 있었음 — `getActiveLineColor()` 대조로 교체.
- 좌표 NaN: 없어진 `TOOLBAR_WIDTH` 를 시험 보조가 아직 참조 — 입력 컨트롤러가 캔버스
bounding rect 를 쓰도록 바뀐 현행에 맞춤.
- 호 각도 비교: 같은 각의 음수·2π 표기 차이라 [0, 2π) 정규화 후 대조.
검증: `npx vitest run` 87 passed / 0 failed, `npm run check-types`·`npm run build` 통과,
`pytest tmp/tests/ -q` 131 passed / 0 failed(신규 `test_b07_title_block_fields.py` 4건 포함).
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>
- `groupId` 를 도면 JSON 에 실어 [저장] 뒤 다시 연 도면에서도 묶음 유지.
엔티티 13종의 `toJson` 대신 직렬화 한 곳(export/import)에서 붙이고,
복원은 순번이 아니라 id 로 — `compact` 가 null 을 걷어 index 가 어긋남.
- `setScreenScale` 의 매 호출 `console.log` 제거 — 줌·팬마다 콘솔을 덮었음.
- 검증: `group-id-roundtrip.test.ts` 신규, `npx vitest run` 87건 중 81 passed,
실패 6건은 작업 전과 동일한 기존 결함. `npm run build` 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
보조 워크트리가 `npm run dev` 맨손 실행에서 메인의 5173 을 뺏던 문제.
`server.port` 를 `FRONTEND_DEV_PORT` 로 읽고 기본값 5173 유지 — main.py 의
CLI `--port` 가 덮는 경로는 그대로임.
포트 판별은 `.claude/dev_up.py`(git 밖)가 담당 — 링크 워크트리는 `.git` 이
파일이라 그것으로 메인(8000·5173)·보조(8001·5174)를 가르고, 자식에
SERVER_PORT·FRONTEND_DEV_PORT·AISLO_API_PORT·AISLO_VITE_PORT 4개 주입.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기슭막이가 관 지점 시설로 옮겨간 뒤(f916aa52) 이관 경로만 옛 스펙에 남아
`POST /route/structures/migrate` 가 400 으로 통째 실패했음 — 저장소가
`managed_by` 타입을 거절하는데 이관은 계속 구조물로 만들었기 때문. 같은 요청에
실린 대피소·기타까지 하나도 이관되지 않았음. 호출부가 B05 진입 경로라 레거시
프로젝트는 진입할 때마다 실패했음.
- B05_Profile_Structures_Migration: 결과를 MigrationPlan(구조물 / 관 시설)으로 나눔.
managed_by 타입은 레지스트리 기본값(사용자 확정분)을 승계한 PipePoint 로 만듦 —
구간은 기준측점 ∓ 전/후
- B05_Profile_Structures_Router: 관 시설분을 pipe_points.json 에 덧붙임. 관 지점
파일이 없으면 그 몫만 미루고 이력도 남기지 않음(원천 측점이 남아 재이관됨)
- common_util_drainage_pipes: read_pipe_points_file · append_pipe_points_file ·
pipe_points_path_in 신설 — 저장 당시 노선 지문 보존
- 관 시설에서 투영된 "기슭막이" 라벨이 되이관돼 "기타" 로 굳던 경로도 함께 막음
- B06_Section_Engine_Revetment 삭제 — 호출자 0건(벽은 관 세트가 만듦)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
사용자 편의성 점검 15건 중 반영 확정분 12건.
편집 손실 방지
- CAD에 미저장 표시(drawingDirty)를 두고, 도면을 바꾸기 전에 묻는다.
전에는 그은 선이 경고 없이 사라졌다(실측: 캔버스 서명이 원본과 동일).
- 도면 전환을 겹쳐 눌러도 늦게 온 응답이 화면을 덮지 않게 가드를 뒀다.
- 도면층 삭제·객체 이동을 되돌리기에 실어 Ctrl+Z로 돌아오게 했다.
확정한 도면은 읽기 전용
- runCommand 한 곳에서 보는 명령만 통과시킨다(허용 명시 방식).
- 도면을 실을 때 앞 도면의 그리기 도구를 내린다 — 켜 둔 도구가 확정본에도
계속 그렸다.
- 확정 버튼 자리를 [현재 도면 확정] / [수정]으로 가른다. 확정 해제는 [수정]
한 곳뿐 — 되돌리기·색 고르기로 확정이 풀리던 경로를 없앴다.
그 밖
- 잠금 도면층(도각·등고선·계류)은 마우스가 스쳐도 하이라이트하지 않는다.
- 준비 중 도면 7종도 빈 도각으로 열린다(눌리지 않는 회색 버튼 제거).
- 도면층 행이 잘리지 않게 패널을 300px로 넓히고 이름을 줄여 담는다.
- 기본 그리기 색을 검정으로 — 흰 종이에 흰 선이라 안 보였다.
- 색 고르는 동안 변경 통지가 연발하지 않는다.
- 수량표는 앞 단계 산출물이라 도면에서 고치지 못하게 막고 안내한다.
- 자동백업 칸을 도면별로 나눈다 — 되살리면 다른 도면에 붙었다.
- 횡단 장 id를 시작 측점 기준으로 바꿔 구간이 달라져도 옛 확정이 안 붙는다.
- 회사 도각 저장 전에 좌표를 검사하고, 기본 도각으로 되돌리기를 연다.
- 횡단 파일이 없어도 나머지 도면 목록은 남는다.
- 불러오는 중·실패를 화면에 표시한다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
좌표계 기록 이전 저장본을 되돌리는 폴백을 넣으면서 변환을 하나로 묶었다. 그래서 도엽
배경(등고선·세류선)까지 저장본 좌표계로 되돌아가, 라벨과 실좌표계가 갈린 사업지에서
배경이 1,000km 밖에 놓였다. 도곽이 그 거리까지 벌어지면 유역도는 사실상 빈 화면이 된다
(다른 PC 보고: 배수유역도 그림 자체가 없음).
변환을 둘로 나눈다.
- 배경·노선: `context.crs`(사업지 .prj 좌표계) — 노선이 그 좌표계에 있다.
- 세부유역: 그 파일을 쓸 때 쓴 좌표계 — 옛 저장본은 노선 CSV의 EPSG 라벨로 쓰였고,
같은 라벨로 되돌려야 원래 미터 좌표가 나온다(왕복이 상쇄된다).
되찾을 근거조차 없을 때(노선 파일 없음 등)를 위해 안전장치를 뒀다. 노선에서 50km 넘게
떨어진 유역은 빼고 경고를 남긴다 — 도면 전체를 잃는 대신 배경·노선은 그린다.
폴백에 쓰는 route_geometry import 는 함수 안으로 내렸다(순환 import 여지 제거).
검증: tmp/tests 44 passed(신규 test_watershed_source_crs.py 2건).
c1bb453f 유역도 API 재확인 — 엔티티 1,597개(등고선 1028·계류 460·배수유역 20·도각 82·
유역 제원 4·노선 1·표제 2)로 수정 전과 같다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
횡단배수 최소 계획고가 두 곳에서 강제됐다. 자동설계는 배관 자리 계획고를 시설
여유만큼 들어 올렸고(B05_Profile_Engine_Sections), 화면은 그 아래로 내리는 편집을
차단했다(blocksMinCover). 배수 자리마다 계획고가 끌려 올라가 제어가 어려웠다.
산식(common_util_drainage_pipes.facility_clearance_m)은 그대로 두고 적용 여부만
enforce_pipe_clearance 스위치 하나로 뺐다. 기본은 해제라 자동 전처리가 계획고를
들어 올리지 않고, 편집도 막히지 않는다. 부족분 경고(minCoverWarningText)는 켜고
끔과 무관하게 계속 뜬다. 스위치는 요청 → DB 저장값 → 해제 순으로 정해지며,
계획선 criteria와 B06 재생성 경로도 확정 당시 값을 따라간다.
3D 예상형상은 계획선 편집마다 500ms 디바운스로 따라오던 것을 없앴다. 편집이
쌓이면 상단 [3D 업데이트] 버튼에 대기 점이 붙고, 누를 때 코리도와 측점선을 한 번에
갱신한다. 최초 진입의 자동 그림은 그대로다.
① 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>
B03~B07 라우터는 로그인·회사 소속만 확인하고 URL의 project_id가 누구 것인지는 보지
않았다. 프로젝트 id만 알면 남의 회사 자료를 읽고 쓸 수 있었고, B07 도각 저장(PUT)은
그 회사의 공용 양식을 덮어쓴다.
require_project_access를 protected_with_company에 함께 걸어 경로에 project_id가 있는
요청만 한 곳에서 검사한다. 다른 회사면 403, 없는 프로젝트면 404, 시스템 관리자는 통과.
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>
프로그램 기본 도각(resources/template_2dDrawing)은 읽기 전용으로 두고, 고친 도각은
회사 도각(storage/{회사}/templates)으로 저장한다. 이후 그리는 도면이 그것을 쓴다.
- Engine_Template: 회사 도각 우선 로더(ContextVar로 요청마다 회사 폴더 지정),
캐시 키에 mtime을 넣어 저장 즉시 반영(템플릿 수정에 백엔드 재시작이 필요 없어짐),
frame_template_document()/save_company_template() 신설.
- Router: GET/PUT /api/projects/{id}/frame-template. 도각은 실치수 1:1로 오가므로
좌표 역변환이 없다.
- UI_FrameEdit(신규): 「도각 편집」 버튼·배너·[완료]/[취소]. 완료 시 도면 캐시를 버리고
보던 도면을 다시 싣는다. 편집 중 변경 알림이 도면 확정을 풀지 않게 막았다.
확정한 도면은 저장본을 그대로 쓰므로 옛 도각을 유지하고, 확정을 풀면 새 도각으로
다시 그려진다(2026-09-01 사용자 확정).
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>
용화 예정노선으로 자동 체인을 돌리자 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>
- state.setActiveLayerId(): 잠금 도면층(도각·원지반) 요청은 잠금 아닌 첫 층으로
돌린다. 브리지·자동백업 복구·JSON 가져오기·도면층 패널·리본이 모두 이 함수를
지나므로 가드를 한 곳에 뒀다. 잠금 아닌 층이 없으면 요청값 유지.
- RibbonWidgets: 「현재 도면층」 목록에서 잠금층을 뺀다(현재 층이 잠금이면 유지).
- Cover 엔진: 표지에 잠금 아닌 「주기」 도면층을 실어 덧그릴 자리를 만든다.
표지는 엔티티가 전부 도각(잠금)이라 가드만으로는 그릴 곳이 없었다.
- state.resetUndoBaseline(): 도면 적재를 되돌리기 바닥으로 세운다. 첫 Ctrl+Z가
도면을 싣기 전 빈 상태로 되돌아가 화면이 백지가 되던 결함을 닫는다.
- debug-hook: 검증용 snap() 창구 추가. state.test.ts 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). 내일 재작업 예정.
B08_Quantity_Proto.py 삭제, 라우터·UI 페이지 이관 전 상태로 복원.
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>
실측 사고(2026-08-31): 실제 업로드에서 자동 설계 체인이 B05 경로 계산 400으로
멈췄다. input/prj/ 에 옛 result.prj(KGD2002 East Belt 2010, FN 600,000)가 남아
있었고, find_project_prj 가 이름 정렬 첫 번째를 골라 새 용화.prj(Korean 1985
Modified East Belt, FN 500,000) 대신 그것을 집었다. 그 결과 노선이 Y로
100,000m 어긋나(로그의 bp y=468228.23, 옳은 값 367920.75) 지표면 밖으로 나갔다.
자료를 다시 올려도 파일명이 다르면 옛 PRJ가 그대로 남는다 - 이름 정렬은 어느
것이 지금 쓰는 것인지 알지 못한다. 그래서 지금 쓰는 지형 자료(LAS/LAZ/TIF/TFW)와
basename이 같은 PRJ를 먼저 찾고, 못 찾으면 가장 최근 것을 쓰고 경고를 남긴다.
폴백 경로에서도 노선 세트 폴더(input/shp/)의 PRJ는 제외한다 - 그건 노선 좌표계다.
검증: 실제 프로젝트 폴더로 확인 - 선택이 용화.prj로 바뀌고 노선 시점이
x=208403.20 y=367920.75 로 TFW 원점(208288.84/368092.28) 안쪽에 앉는다.
tmp/tests/test_route_shapefile_input.py 10개 통과(회귀 시험 추가), 전체 302 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
노선 파일이 다섯이면 카드도 다섯이어야 어느 것이 왔는지 보인다(사용자 지시).
세트를 슬롯 하나에 몰아 담던 companions 구조를 걷어내고, 슬롯 하나가 파일 하나를
갖는 기존 구조로 되돌렸다 - 업로드 루프도 원래대로다.
- 왼쪽 컨테이너 계획노선 자료: csv(.csv/.shp) shx dbf cpg route_prj
- 오른쪽 컨테이너 지형 자료(LAS): las_laz prj(지형 좌표계) tfw tif + LAS 없는 설계 토글
- .prj만 확장자로 안 갈린다 - 노선 도형과 basename이 같으면 노선 좌표계 카드,
아니면 지형 카드(planSlotAssignments). 재접속 현황은 저장 경로(input/shp/)로 가른다.
- 재접속 현황 응답에 relative_path 추가.
- 필수 판정이 노선 PRJ를 route_prj로 따로 센다 - 지형 PRJ 없이 통과하던 구멍을 막았다.
- 형제 카드(shx/dbf/route_prj)는 노선 도형이 shapefile일 때만 필수 - 확장자 줄의
"선택" 꼬리표가 실시간으로 붙고 떨어진다.
- 카드가 좁아져 한글 제목이 글자 단위로 접히던 것을 word-break: keep-all과 헤더
flex-wrap으로 고쳤다.
화면 검증(공용 브라우저): 실물 7파일을 한 번에 떨어뜨려 배정 실측 - route.prj는
노선 좌표계 카드, terrain.prj는 지형 카드로 갈렸고 카드 제목 9개 모두 한 줄.
tmp/tests/test_route_shapefile_input.py 9개 통과, tsc --noEmit 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
표지는 DRAWING_GROUPS에 kind 없이 라벨만 있어 화면에서 열 수 없었다. 실무 설계도면
원본(울진 소광리, A3) 1쪽의 잉크 bbox를 pymupdf로 mm 실측해 A1로 2배 환산했다.
원본에 도각이 없다 — 좌우 테두리 없이 상·하 굵은 가로선 두 줄뿐이라, 사용자가 고른
"도각 없는 전면 디자인"이 실무 그대로였다.
- resources/template_2dDrawing/00_template_cover.json 신설. 엔티티 14개(재단 표식
Point 4·띠 Hatch 3·글자 Text 7), bbox가 정확히 A1(840x594). 좌표계는 00_template_A1
과 같아 도각 도면과 나란히 놓인다.
- 굵은 띠는 solid Hatch로 냈다. lineWidth는 캔버스 화면 픽셀이라(screenCanvas
.drawController.ts:261) 확대해도 두꺼워지지 않아 실치수를 못 낸다.
- 글자 크기는 잉크 높이가 아니라 폭으로 잡았다. 원본은 장평이 좁은 CAD 글꼴이라
높이를 그대로 옮기면 위치값이 종이 밖으로 44mm 넘치고 라벨과 겹친다.
- B07_DesignDetail_Engine_Cad_Cover.py 신설. frame_entities()를 쓰지 않는다 —
_transform_entity()가 Hatch의 points 배열을 못 옮기고, 표지는 A1 실치수 고정이라
애초에 변환이 필요 없다. 공용 함수는 건드리지 않았다.
- kind "cover" 배선: Schema Literal, Api_Fetch, UI_Page DRAWING_GROUPS,
Router_Support 목록·빌드 분기·확정 캐시 매핑.
값(공사명·위치·사업량·시행청)은 아직 빈칸이다 — 메타 배선은 다음 판.
검증: tmp/tests/test_cover_template.py 6건 통과(A1 치수·Hatch 두께·잠금 레이어·
치환·lru_cache 오염 없음). 공용 브라우저에서 표지를 열어 엔티티 14개 전부
b08-frame, Ctrl+A 선택 0 확인.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
카드의 파일 선택 버튼은 단일 선택이라 .shp 하나만 골라졌다. 형제 파일이 없으면
GDAL도 못 열고 필수 세트 판정도 막힌다. .shp를 받는 슬롯만 multiple로 연다.
화면 검증(공용 브라우저): 계획노선 카드 accept=".csv,.shp" multiple=true,
나머지 슬롯은 종전대로 단일 선택 유지.
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>
origin/backend 브랜치를 합치면서 딸려 온 pushtest(2026-07 push 확인용,
내용 "evidence to proof the push")를 정리한다. 브랜치 이력은 합쳐진
상태로 남으므로 원격 브랜치를 지워도 이력 손실이 없다.
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>
패널이 상태막대 바로 위에 떠 있어 명령행과 겹쳤고, 명령 입력칸을 클릭으로
누를 수 없었다. bottom에 --cad-command-height를 더해 명령행 위로 띄운다.
명령행을 접으면 이 값이 0이라 예전처럼 상태막대 위에 붙는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞선 감사가 불완전했다. 같은 부류로 도각·원지반을 건드리던 곳이 더 있었다.
- select-tool.helpers pickEntityAt: 클릭 선택 후보에 잠금 객체가 섞여
도각을 물면 아무 일도 안 일어나는 죽은 클릭이 됐다.
- eraser-tool: 자르기의 교차점 계산이 도각 선을 절단 경계로 썼다.
- property-tools OVERKILL: 선택이 없으면 전체가 대상이라 도각까지 지웠다.
- text-tools 찾기/바꾸기: 도각 표제란 글자를 바꿔 버렸다.
- selection-tools 유형/유사 선택: 잠금 객체까지 세어 토스트 개수가 틀렸다.
전부 getPickableEntities()로 바꾼다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
노선이 유역 해칭 아래에 깔려 보이지 않았다. polyline_entity에 width를 더해
노선만 굵게(3) 그리고, 그리기 순서를 해칭·정보표 다음으로 옮긴다. bbox
계산에는 그대로 포함시켜 오른쪽 칸 위치는 흔들리지 않는다.
오른쪽 칸을 방위표(맨 위) → 유역 정보표(아래로 쌓기) 순으로 바꾸고 방위표를
14mm에서 26mm로 키운다. 도면 오른쪽 위 구석에 따로 놓던 옛 배치는 걷어낸다.
등고선·세류선 절취가 경계 바깥 점을 하나씩 물고 나와 외곽이 들쭉날쭉했다.
Liang-Barsky 선분 절취로 바꿔 끝점이 경계 위에 정확히 놓이게 하고, 그만큼
두었던 물림 여유(_CLIP_SLACK 14mm)를 없앤다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
도면층 17종을 프로젝트가 이미 쓰는 용어로 맞춘다 — 원지반·계획선·구조물·
암 경계선·수량 산출표·도각·등고선·계류·노선·배수유역·유역 제원·표제·
그래프 격자·측점표·유토곡선·그래프 축·운반 구간. CAD 앱의 'Default'와
'New layer'도 기본·새 도면층으로 바꾼다. layerId는 그대로다.
방위표 템플릿(00_template_compass.json)에 원본 DXF의 표제란 글자 4개가
도면 반대편에 남아 있었다. 이 때문에 템플릿 bbox가 183x262mm로 부풀어,
긴 변을 14mm에 맞추는 compass_entities()가 방위표를 1.1x1.8mm로 줄여
놓고 있었다. 글자를 지우니 bbox 20.5x32.9mm, 방위표가 8.7x14mm로 나온다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
잠금 필터가 공용 함수 없이 호출부마다 들어가 있어 넣은 곳은 막히고 안 넣은
곳은 샜다. Ctrl+A는 도각 103개·원지반 16개까지 전부 선택했고, 선택 삭제에는
잠금 검사가 없어 Ctrl+A → Delete 한 번에 도각이 사라졌다. 자르기와 트림·모따기
계열, 커서 강조도 도각을 집었다.
state.ts에 getPickableEntities()(잠금 도면층 제외)를 두고 집기 경로가 그걸
쓴다. 선택은 setSelectedEntityIds() 한 곳에서 막아, 앞으로 생길 선택 경로도
자동으로 잠금 객체를 담지 못한다.
화면맞춤(zoomToFitScreen)도 같은 목록에 맞춘다 — 도각까지 넣으면 A1 한 장
전체가 잡혀 화면의 40%가 여백으로 갔다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.cad-inspector의 bottom이 --cad-command-height를 포함해서, 상태막대의
[명령행] 토글이 좌측 팔레트 높이까지 함께 바꿨다. 좌측 패널이 상위이므로
상태막대까지 내려 명령행 표시 여부와 무관하게 만든다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
화면 좌표는 아래에서 위로 재는 규약인데(getCanvasPoint·drawLineScreen), 화면 사각형
채우기만 캔버스 y로 옮길 때 높이를 빼지 않았다. 그래서 아래 변에 놓아야 할 사각형이
높이만큼 아래로 내려가, 선택 그립이 선보다 8px 밑에 찍혔다. 화면 픽셀 단위 어긋남이라
줌을 해도 간격이 그대로였다.
커서 옆 입력칸은 같은 함수를 쓰므로 자리가 유지되도록 높이만큼 내려 잡았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
선택 아이콘을 누르고 우클릭하면 커서 옆 안내가 사라졌다. 우클릭은 ENTER이고,
선택 도구는 ENTER로 최종 상태(SELECTION_COMPLETED, type: final)에 들어가 멈춘다.
그 끝남은 move·copy·rotate가 선택을 자식으로 불러 쓸 때 onDone으로 필요하므로 두고,
선택이 그 자체로 활성 도구일 때만 새로 세운다.
커서를 따라다니는 글자가 18px이라 컸다. 쓰이지 않던 상수
CANVAS_INPUT_FIELD_FONT_SIZE를 12로 낮춰 두 곳이 함께 쓰게 했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
desynchronized+alpha:false로 합성 큐를 건너뛰게 했더니 일부 스냅이 동작하지 않는다는
지적. 커서 체감보다 스냅 정확도가 먼저다. 화면 캔버스를 원래 getContext('2d')로 되돌린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
도면을 열 때마다 0.8~1.0초가 걸렸다. 서버가 요청마다 도면을 새로 조립하고(0.3~0.6초),
구조물 모듈이 공유 캐시를 건너뛰고 종횡단 상세를 매번 다시 받았기 때문이다(0.2초).
진입 후 첫 장을 띄우고 나서 목록 전체를 배경에서 받아 캐시에 담고, 구조물 모듈은
B05·B06과 같은 Section_Store를 쓴다. 클릭에서 렌더까지 55~65ms로 줄었다.
CAD 알림은 react-toastify라 프로젝트 공용 토스트와 모양이 달랐다. 호출부 30여 곳을
그대로 두려고 vite alias로 라이브러리 자리에 다리를 끼워, 부모가 있으면 메시지로 넘겨
부모 토스트로 띄운다. 백업 되살리기처럼 누르는 안내를 위해 showToast에 onClick을 더했다.
커서가 미묘하게 끌리던 것은 프레임이 느려서가 아니라(16.7ms, 60fps) 합성 파이프라인
때문이다. 화면 캔버스를 desynchronized+alpha:false로 만들어 그 큐를 건너뛴다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
계획노선 CSV는 설계 원청이 지정한 기준선이라 프로그램이 바꾸면 안 되는데,
격자 Dijkstra가 제어점 사이에 셀 중심점을 끼워 넣어 평면이 틀어졌다.
제어점은 원좌표로 되박히지만 끼어든 점은 되박히지 않기 때문이다.
원인은 셀 크기와 정점 간격의 기하 관계다. LAS 프로젝트는 비용면 셀이
2.188m(원본 DTM 해상도, 다운샘플 배수 1)라 정점 간격(평균 2.61m·최대
3.25m)이 전부 8이웃 한 걸음에 들어가 중간점이 안 생겼다. 도엽 등고선
프로젝트는 DTM이 1.0m라 배수 2로 셀이 2.0m가 되고, 20쌍이 두 걸음으로
벌어져 22개 점이 끼어들었다(최대 이탈 2.08m, 평면 +4.72m, 곡선 경고 증가).
솔버에 LAS 전용 보존 코드는 없다 — 셀 2.0m에 LAS CSV를 스냅해도 두 걸음
쌍이 16개 생긴다.
제어점 쌍의 평면 거리가 ROUTE_DIRECT_LINK_CELL_FACTOR×셀 이하면 탐색을
건너뛰고 두 칸을 그대로 잇는다. 조밀 기준선(정점 전체가 제어점)은 평면이
원본과 완전히 같아지고, 간격이 문턱을 넘는 구간만 종전대로 탐색한다.
경사·곡선 제약은 이 구간에서 경고로만 남는다.
검증(프로젝트 0aca395f 실데이터, 원본 미변경 사본에서 재탐색):
- 셀 2.0m: 정점 156→135, 끼어든 점 22→0, 이탈 2.076→0.000m,
평면 354.84→350.11m(CSV와 동일), 곡선 경고 9→8
- 셀 3.0m: 111→135 정점(같은 칸에 합쳐지던 24개도 살아남음), 이탈 0.000m
- LAS 프로젝트 c1bb453f: 재탐색 결과 xy 차 0.000000m — 결과 불변
- pytest 14 passed (직결·같은 칸 미병합·먼 구간 탐색 유지 3건 추가)
사용자 지적(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(투영 이월·구 저장분 폐기 테스트 추가).
/surface/confirmed가 지형 가장자리를 structured.npz 또는
ground_points_{filter}.npz에서만 읽었다. 둘 다 LAS 산출물이라 LAS 없이
설계한 프로젝트에는 없고, bounds가 null로 나가 B05가
"지표면 범위 정보를 찾을 수 없습니다"로 막혀 3D 지형을 못 띄웠다.
두 파일이 없으면 확정 모델 격자(npz)의 bounds를 그대로 쓴다. 도엽 서피스
npz는 이미 같은 형식의 bounds(3×2)를 담고 있다.
검증: 프로젝트 5d18ebe3(LAS 0개) B05 재진입 — /surface/confirmed 200,
preview·contour 요청이 뒤따라 나가고 안내문이 사라짐. 지형·등고선·코리도·
구조물 라벨까지 화면에 뜬 것 확인(152_b05.png).
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} 그대로 사용.
사용자 보고(2026-08-30): 체크한 상태에서 실수로 넣은 LAS가 그대로 업로드됐다.
카드에 --disabled 클래스만 걸려 있어 회색으로 보일 뿐 선택 버튼과 input이
살아 있었고, 서버도 las_free일 때 LAS 1개를 정상 경로로 받아 줬다.
- 화면: 선택 버튼·file input을 실제로 잠그고, 켤 때 이미 고른 LAS는 내린다.
파일 선택 영역·드롭으로 들어오는 것은 onFileSelected에서 걸러 낸다.
- 서버(신뢰 경계): /files는 las_free면 LAS 0개만 받는다. 큰 LAS가 타는
청크 업로드 세션 생성도 같은 검사를 건다 — 여기서 막지 않으면 /files 검사를
통째로 비켜 간다.
`storage/`를 심볼릭 링크·정션으로 두면(워크트리를 나눠 쓰면 실제로 그렇다)
프로젝트 루트는 링크 경로 그대로인데 저장 엔진(resolve_upload_destination·
resolve_chunk_session_dir)은 Path.resolve()로 링크를 따라가 실경로를 만든다.
그래서 chunk_path.relative_to(project_root)가 ValueError로 터졌다.
'...본폴더\storage\1\3\{id}\B03_FileInput\chunks_temp\...\00000000.chunk'
is not in the subpath of '...워크트리\storage\1\3\{id}'
프로젝트 루트와 임시 보관함 루트를 realpath로 돌려주어 기준을 하나로 맞춘다.
경로 검증(상대경로·.. 금지·루트 이탈)은 그대로다.
검증: tmp/tests/test_storage_symlink_root.py 추가 — 링크된 storage에서 반환
경로가 resolve() 결과와 같고 relative_to가 성립하는지. 전체 9 passed.
구조물을 세워도 설계선이 원래대로 지나갔다. 정본(design_line)에 담긴 건 트림 전
원본이고, 구조물이 깎아 내는 부분은 B06이 그릴 때 계산하기 때문이다. 수확기가
appendCrossDesignOverlay를 구조물 layout의 designTrim과 함께 태우고, 그 블록의
서버 설계선은 걷어낸다. 걷어낼 자리를 알도록 서버가 블록 테두리를 함께 내려준다.
곧은 사면 하나가 CAD에서 선 열댓 개였던 것은 B06이 설계선 점 사이마다 line 하나를
만들기 때문이다. 수확한 뒤 끝점이 맞물리는 선분을 잇고 공선점을 지운다(RDP).
측점 20m 18조각 → 폴리라인 3개(선분 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 사용자 확정. LAS 없이 설계할 때 WF1이 자동 확정하는
sheet_* 모델이 이 값을 따른다(B03_FileInput_Service_WF1).
라플라스는 5m 선 재추출 평면 이탈이 2m 초과 2.6%·95% 0.74m 로 6종 중
가장 작고, 평탄 셀도 0.235%로 TIN 계열(8~10%)보다 낮다.
사용자 지시(2026-08-30): IDW·거리비례·TIN 곡면 제거, 5m 도엽등고선 기준 TIN 추가.
TIN(도엽선)은 격자에 구운 라인 셀이 아니라 벡터 등고선 정점의 원좌표를 그대로
Delaunay로 잇는다. 격자 밖은 100m 여유만 물고 자르며, 길이 필터는 두지 않는다
(짧은 봉우리 폐합 링을 버리면 마루가 통째로 평평해진다).
거리비례는 버튼에서만 빼고 함수는 남긴다 — 라플라스·TPS·ANUDEM·다중해상도가
초기추정으로 계속 쓴다. 기본 방식은 distance가 빠져 multires로 옮겼다.
검증: 실데이터 767×840·1m 6종 93.5s. 서피스에서 다시 뽑은 5m 선의 평면 이탈이
TIN(도엽선) 중앙 0.12m·2m초과 8.1%로 6종 중 최소(직전 최선 다중해상도
0.34m/18.3%). pytest 8 passed(격자와 어긋난 링 위 표고 재현 테스트 추가).
여백이 많다는 지적. 도각 안쪽 작도영역(770x594)에서 5%를 떼고 배치해 사방에
70mm 가까이가 비었다. 2%로 줄이니 같은 블록으로 3장이 2장이 된다(16/7).
표가 제각각 높이에 뜨던 것은 블록을 칸 한가운데 놓았기 때문이다. 수량표 높이는
모든 블록이 같으므로 칸 아래 변에 맞춰 놓으면 표가 한 줄로 선다. 단면 높이 차이는
위쪽이 흡수한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DTM 스무딩(184296f5)이 표고 정본(npz)에서 이미 가우시안+B-spline을 걸므로
프리뷰에서 다시 NURBS를 적합하면 곡면 적합이 이중으로 걸린다. c2060f3b 이전의
격자 메시로 되돌리고, 쓰지 않게 된 NURBS import를 지운다.
검증: 프리뷰 glb 정점 표고 = npz 격자 표고 (|차이| 최대 0.0000m,
distance 원본·스무딩 각 161,280정점).
구조물(배수관·기슭막이·세월교·BOX암거·물넘이포장)이 횡단도에서 빠져 있었다.
기하 산식이 전부 B06 화면(TS)에 있어 서버가 도형을 만들 수 없었기 때문이다.
파이썬으로 옮겨 적는 대신, 화면에 붙이지 않은 SVG에 B06 그리기 함수를 그대로
불러 그린 뒤 그 도형을 CAD 엔티티로 옮긴다. 산식은 한 벌로 남고, 앞으로 B06에
붙는 구조물·치수·글자는 손대지 않아도 도면에 따라온다.
자리를 맞추려면 실좌표(m)를 종이(mm)로 옮긴 값이 필요하다 — 서버가 측점별
cross_placements로 실어 보낸다.
장 배치는 전체 블록의 최대치로 칸을 통일하던 것을 버리고, 블록 크기를 먼저 재서
가장 많이 담기는 행 수를 고른다. 열폭은 그 열의 최대폭, 행높이는 그 행의 최대높이라
행·열은 그대로 맞는다. 같은 노선에서 4장이 3장이 됐다(12/6/5).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LAS DTM 스무딩과 같은 두 단계를 계수 그대로 옮겨 방식마다
{stem}_smooth.npz·_smooth_preview.glb를 만든다(계수 변경 금지 — 사용자 지시):
정규화 가우시안(SURFACE_SMOOTHING_DTM_SIGMA_M) → C2 바이큐빅 B-spline 재평가
(kx=ky=3, s=SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH). smooth_dtm()은 라이다
발자국(TerrainContext)을 받아 그대로 못 쓴다.
- 결측이 하나라도 있으면 스플라인 결과가 통째로 NaN이 된다(TIN·TIN 곡면은
볼록껍질 밖이 결측). 최근접 표고로 메워 적합하고 원래 마스크로 되돌린다.
- 재평가 격자는 원본보다 성기게 잡지 않는다 — 이 npz는 스무딩 확정 시 종·횡단이
샘플링하는 표고 정본이라 화면 정점 상한으로 해상도를 깎으면 안 된다.
메시 정점 수는 _preview_mesh가 알아서 줄인다.
- 화면: 방식 버튼 줄에 스무딩 드롭다운을 놓고 기본값을 켜 둔다.
실측(c1bb453f, 8종): 격자 767x840 1m 유지, 거칠기 감소(거리비례 0.341→0.217,
TIN 0.067→0.051), 표고 변화 3~33mm. build_surface_sampler(smooth=True) 정상.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
격자 삼각형을 그대로 이으면 1m 셀 경계가 톱니로 보인다. LAS NURBS 모델이 쓰는
fit_nurbs_spline·evaluate_nurbs_spline을 그대로 걸어 곡면을 평가한 뒤 메시를
만든다. 제어 간격은 같은 config(SURFACE_NURBS_PATCH_SIZE_M ÷ 축당 제어점 수,
최소 1셀). 무효 셀은 최근접 표고로 메워 적합하고 원래 유효 마스크로 다시 도려낸다.
적합이 실패하면 격자 메시로 되돌린다.
표고 정본(npz)은 격자 그대로다 — 종·횡단·배수는 격자를 샘플링하므로 계산값에
영향이 없다. 실측에서도 8종 지표가 모두 동일했다. 생성 83s → 108s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- ANUDEM형: 등고선의 국소 최대 곡률점(코너)에서 능선·계곡 구조선을 뽑아
(굽은 안쪽이 높으면 계곡, 낮으면 능선) 이웃 표고끼리 이어 앵커로 심고,
박판 스플라인으로 풀고, 형태학적 재구성으로 가짜 웅덩이를 메운다.
Topo to Raster가 밟는 세 단계를 그대로 옮긴 것. 메운 웅덩이는 평탄해지지
않게 안쪽으로 cm 단위 경사를 남긴다(Garbrecht·Martz 평탄면 해소).
- 다중해상도: 1/8→1/4→1/2→1 격자로 내려오며 앞 단계 해를 시작값으로 쓴다.
- 박판 솔버를 solve_min_curvature()로 분리해 두 방식이 함께 쓴다.
- 화면 버튼 순서에 두 방식을 넣었다(버튼이 곧 하이드·쇼).
실측(c1bb453f, 8종 83s) 평탄 셀: TPS 0.003 / 다중해상도 0.003 /
거리비례 0.014 / TIN 곡면 0.026 / ANUDEM형 0.046 / 라플라스 0.235 /
TIN 선형 10.36 / IDW 69.13 %. LiDAR 대비 |dz|는 2.59~3.00m로 차이가 작다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
문헌(ANUDEM Hutchinson 1988/89, DEM 보간 비교연구)은 지형에 따라 우열이
갈려 단일 최적해가 없다고 본다. 방식을 하나로 고르지 않고 전부 만들어 두고
사용자가 눈으로 비교해 정하도록 했다(2026-08-30 사용자 지시).
신규 B04_PreProcess_Engine_SheetMethods.py — 거리비례 / TPS(박판, 감쇠
최소곡률 LSMR) / 라플라스 / TIN 선형 / TIN 곡면(Clough-Tocher) / IDW.
방식마다 dtm_sheet_{key}.npz + 프리뷰를 만들어 surface_models에
source_filter=sheet_{key}로 등록하므로 기존 뷰어·등고선·프리뷰 경로가 그대로
돈다. 폐합 링 안쪽 처리는 방식과 무관하게 똑같이 적용한다.
화면: 도엽등고 3D 서피스 컨테이너에 방식 전환 버튼과 '라이다 겹쳐 보기'
토글(반투명 파랑)을 달았다. 라이다는 같은 좌표계라 같은 자리에 겹친다.
실측(c1bb453f, 6종 65s): 평탄 셀 TPS 0.003% / 거리비례 0.014% /
TIN 곡면 0.026% / 라플라스 0.235% / TIN 선형 10.4% / IDW 69.1%.
LAS 대비 노선 |dz| 평균은 2.59~3.00m로 방식 간 차이가 작다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 집기 반경이 월드 거리였다. HIGHLIGHT_ENTITY_DISTANCE를 월드 거리와 그대로 비교해
확대할수록 화면상 반경이 같이 커졌다(1369%에서 15월드 ≈ 205px). helpers/pick-radius로
확대율을 나눠 쓰고 상수를 화면 픽셀 10으로 낮췄다. 하이라이트·선택·그립·시퀀스 도구가
같은 반경을 본다.
- 누른 채 끄는 선택이 없었다. 왼쪽 버튼은 mouseup에서만 클릭을 보내 누르고 끌어도 점이
하나만 찍혔고, 그래서 좌→우(창)·우→좌(교차) 구분이 먹히지 않는 것처럼 보였다.
선택 도구일 때만 누를 때 첫 점을 보내고, 4px 넘게 끌고 놓으면 그 자리로 사각형을 닫는다.
다른 도구는 예전처럼 놓을 때 한 점만 받는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 지적(1m 보간선이 등간격 아님)의 원인 세 가지를 잡았다.
- 등고 라인 래스터가 all_touched=True라 2px 두께였다. 그 폭만큼 정확히 5m
배수 표고인 평탄 띠가 생겨(격자의 7.6%) 사이 1m 선 간격이 찌그러졌다.
서피스 전용 얇은 래스터화(all_touched=False)로 바꾸고, 봉우리 폐합 링이
사라지지 않도록 길이 필터도 뺐다.
- 계곡 앵커를 1m로 양자화해 제약에 섞은 탓에 제약 표고가 29단→121단이 되어
계곡 주변만 1m 간격이 됐다. 앵커를 뺐다 — V자 등고선이 계곡 하강을 이미
담고 있어 거리 보간만으로 충분하다.
- 등고선 고정 완화를 수렴시키면 harmonic 해가 되어 마루가 눌린다. z=r은
biharmonic이라서다 — ANUDEM/Topo to Raster가 라플라스가 아니라 thin plate
spline을 쓰는 이유(Hutchinson 1988/89, 조사 결과). 다듬기를 omega=1·5회로
줄였다. 합성 원뿔 검증: 거리보간 오차 0.121m·간격 CV 2.18 → 5회 0.118m·1.43,
20회 이상 CV 50↑ 악화. 원뿔 회귀 테스트를 남겼다.
실측(c1bb453f): 평탄 셀 0.00%, 5m 구간 표고 분포 8.4~10.3%(균등),
생성 6.3s, 노선 |Δz| 2.64m.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
메시(TIN)로 먼저 만들고 거기서 등고선을 뽑던 순서가 계단의 원인이었다.
사용자 지시대로 2D를 먼저 완결한다:
- Delaunay TIN 제거. 표고별 거리장에서 각 셀의 가장 가까운 두 '서로 다른'
등고 라인을 찾아 z=(L1·d2+L2·d1)/(d1+d2)로 보간한다. 같은 표고 정점
3개짜리 평탄 삼각형이 없으므로 계단이 원리적으로 생기지 않는다.
- 폐합 등고선 안쪽은 거리보간이 분화구를 만들므로, 안에 다른 제약이 없는
영역을 마루/웅덩이로 보고 바깥 사면 경사로 ±(간격-0.5m)까지 연장한다.
- 계곡 구조선 앵커는 1m 양자화해 같은 격자에 제약으로 굽는다.
- 등고 라벨 40개 상한(긴 것부터) — 1m 등고선이 잘게 쪼개져 라벨 수백 개가
매 프레임 재계산되며 화면이 멈췄다(사용자 보고). 61 FPS 회복.
실측(c1bb453f): 평탄 셀 2.48%→0.02%, 최대 평탄 덩어리 2406→912㎡,
경사 2% 미만 셀 17145→14444, 생성 36.5s→8.0s, 노선 |Δz| 2.76m.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
패널 줄이 가로 스크롤 상자(overflow-x: auto)라, CSS 규칙상 세로도 auto로 계산되어
슬라이드아웃 메뉴가 리본 높이에서 잘렸다. 캔버스에 가려 보이는 증상이 이것이다.
메뉴를 화면 좌표(position: fixed)로 띄우고 라벨 버튼의 위치에서 자리를 잡는다.
오른쪽·아래로 넘치지 않게 좌표를 물리고 높이를 72vh로 제한했다.
캔버스 위에 뜨게 됐으므로 바깥을 누르면 닫히고, 다른 패널을 열면 먼저 열린 것이 닫힌다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞서는 등거리 '점'을 5셀 간격으로 흩뿌려 점 사이가 벌어진 곳에 평탄
삼각형이 남았고 한 단계(+2.5m)만 넣어 그 안에서 계단이 남았다.
사용자 지시(2D에서 사이 등고선을 먼저 만들고 3D화)대로 등거리선을
연속 셀 선으로 뽑아 새 레벨로 등록하고, 그 선들 사이를 다시 이분한다
(SHEET_SURFACE_MIDLINE_ROUNDS=2 → 5m 주곡선이 2.5m·1.25m 가상 등고로).
정점 간격도 5셀에서 2m로 좁혔다. 거리장은 라운드 간 재사용한다.
실측(c1bb453f): 평탄 셀 3.96%→2.48%(최초 9.35%), 경사 2% 미만 셀
27163→17145, 노선 |Δz| 2.56m 유지. 생성 16.4s→36.5s — 라운드 수는
config로 조절 가능.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
표가 DXF의 표로 나가야 한다(사용자 확정). 지금까지 도면의 표는 선과 문자 뭉치라
내보낼 때 고를 수 있는 길이 하나뿐이었다.
- TableEntity: 열별 폭·행별 높이·칸 문자·병합을 한 객체가 들고 있다. 격자선은 담지
않고 병합 자리에서 선을 끊는 규칙을 표가 스스로 안다(helpers/table-geometry.ts).
회전·대칭은 지원하지 않는다 — 표는 축에 붙어 있다.
- 명령: TABLE을 표 객체 생성으로 다시 쓰고 TABLEEDIT(칸 문자)·TABLEROW·TABLECOL·
TABLEMERGE·TABLEUNMERGE를 더했다. EXPLODE는 표를 선과 문자로 흩는다.
- 그립: 좌측 상단으로 표를 옮기고, 열·행 경계로 폭·높이를 바꾼다.
- 백엔드: 유역 정보표와 횡단 수량 산출표를 표 객체로 낸다. 횡단표는 머리행이 폭
8등분, 본문이 11열 가중치로 격자가 서로 달라 두 경계를 합친 18열로 만들고 병합으로
원래 칸을 되살렸다 — 손으로 하던 가로선 끊기가 사라졌다.
- 수량 역추출: 값 Text의 결정적 id로 읽던 것을 칸에 실은 key로 읽도록 옮겼다. 이미
저장된 도면을 위해 옛 방식을 폴백으로 남겼다.
토적도·종단표는 값이 칸이 아니라 측점 위치에 놓이는 성격이라 이관하지 않았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5m 등고선만 보고 보간하던 것을 구조선 기준으로 보강(2026-08-30 사용자 확정):
- 계곡: 도엽_하천중심선을 기준선으로, 등고선 교차점을 Z 앵커 삼아 선 길이
비례로 보간한 정점열을 TIN에 추가 — 계곡 바닥이 등고선 사이에서도
연속으로 내려간다 (실데이터 605정점, 480~599m)
- 능선: 마루 캡을 고정 +2.5m에서 마지막 등고선 바깥 사면의 국소 경사
연장으로 교체(상한 +간격−0.5m, 경사 불명 시 돔 폴백) — 마루가 주변
사면 기울기를 따라 이어진다
실측: 평탄 셀 5.14%→3.96%(최초 9.35%), 노선 |Δz| 불변(2.57m), 생성 16.4s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
다음 등고선이 없는 봉우리·능선 마루는 TIN이 마지막 등고 표고로 평탄하게
채워 'ㅜ'가 갑자기 'ㅡ'로 변하는 단이 생긴다(2026-08-30 사용자 지적).
등표고 평탄 컴포넌트 중 바깥 유효 이웃이 전부 낮은 것(=마루)만 골라,
경계 거리 비례로 중심 골격이 +간격/2(주곡선 5m 기준 +2.5m)가 되게 올린다.
실제 마루는 마지막 등고와 +간격 사이가 보장되므로 오차 상한이 데이터
불확실성 이내다. 골짜기 바닥은 배수 방향을 모르므로 불변.
값별 사전 필터와 컴포넌트 bbox 창 연산으로 전체 생성 11.8s 유지.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
등고선 정점만의 Delaunay TIN은 같은 표고 정점 3개짜리 평탄 삼각형이
굴곡부·능선에서 계단(terrace)을 만든다(2026-08-30 사용자 지적).
인접 표고 등고선 쌍의 등거리 중간선을 EDT로 찾아 평균 표고 정점으로
추가하되, 멀리 있는 다른 표고 쌍에 우연히 등거리인 지점을 거르기 위해
'그 지점의 최근접 등고선이 바로 그 쌍'일 때만 인정한다.
실측(c1bb453f): 평탄 셀 9.35%→5.31%, LAS 대비 노선 |Δz| 평균
2.65→2.57m·최대 13.11→13.07m. 최고 등고선 안쪽 봉우리·같은 표고 사이
골짜기 바닥은 원천 데이터 한계로 평탄 유지.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>