Files
Aislo/docs/raw/plans/2026-09-07_plan_completed_items.md
T
eomsangdonandClaude Opus 5 eb30b774f8 chore(docs): docs 폴더 git 추적 전환 · PLAN·OWNERS 최상위 이관
- `docs/` 를 .gitignore 에서 빼 git 추적 대상으로 전환 (위키·완료 이력·검증 기록)
- `docs/raw/PLAN.md` · `docs/raw/OWNERS.md` 를 저장소 최상위로 이동 후 .gitignore 에 등록
  — 창끼리 공유하되 저장소에는 안 올리는 장부
- 살아 있는 경로 참조 7개 파일 정정 (아카이브 107건은 그때 사실이라 그대로 둠)
- graphify 날짜별 산출물(`docs/wiki/graphify-out/20*/`) 제외 — `graphify update` 가 다시 만듦

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 18:19:07 +09:00

1004 lines
102 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 완료 항목 이관 — 2026-09-07
> **무엇** — 상시계획서 `docs/raw/PLAN.md` 에서 **체크가 전부 끝난 절 24개**와 그 절들이 낳은
> **자체검증 기록 3건**을 옮긴 것. 계획서가 1,900줄을 넘어 읽기 어려워져 갈랐음.
>
> **원문 그대로** — 절 본문은 한 글자도 고치지 않았음. 바꾼 것은 **묶는 순서와 구획 이름**뿐이고,
> 페이지·기능별로 다시 모은 뒤 「무엇이 끝났나」 표를 앞에 붙였음.
>
> **절 번호는 계획서 것을 그대로 씀** (0-4 · 1-1 · 3-5 …). 계획서에 남은 절들이
> 「0-3 참조」·「3-2 위에 얹음」처럼 이 번호로 서로를 가리키므로 다시 매기면 끊김.
>
> **이관 시점의 계획서** — 남은 항목 **21건** (0-1 · 0-2 · 0-5 · 0-6 · 0-7 · 0-8 · 0-11 ·
> 2-1 · 2-5b · 3-6 · 3-7).
>
> **앞선 아카이브** — 재구획 직전 원문 `2026-09-06_plan_before_regroup.md`,
> 그 전 완료분 `2026-09-04_plan_snapshot_before_cleanup.md` 및 같은 날짜 완료 아카이브 3개.
---
## 무엇이 끝났나 — 한눈에
| 절 | 무엇을 끝냈나 | 주요 커밋 |
|---|---|---|
| **0-3** | 노선 자동탐색 입구를 화면에서 내리고, 측점마다 평면 곡선반경 R 을 실어 법정표 위반을 **표시만** 하게 함 | — |
| **0-4** | 계산 자리 일원화 — 구조물 기하·폐회로 면적·유토곡선을 **Node 진입점 한 벌**로 묶어 서버가 초기값·[저장]·[확정]에서 같은 코드로 계산 | — |
| **0-10** | 계획노선 폴리라인화 — 노선을 **세 벌**(예상 · 초기 폴리라인 · 수정본)로 나누고, 노선 변경 뒤 재확정 체인이 끊기던 것과 값이 조용히 사라지던 것들을 고침 | `356b3216` `9b93d305` `00decc81` `eec29a90` `53525a1d` `674ca827` `908c75d5` `41852da3` `19444149` |
| **0-12** | 계획노선을 「직선 → 곡선 → 직선」으로 — 이어진 꺾임을 한 곡선으로 묶어 **예정노선에 가장 가까운 반지름**을 고르고, 화면에서 곡선을 지우고 넣고 R 을 못박게 함 | `db700496` `e593378d` `9626d9e8` `f59932de` `56a6d186` `43d55f49` `20f34390` `a0bd9c87` |
| **1-1** | B01 권한·회사·프로젝트 연결 정비 — 시스템 회사 자동 권한 · 회사 관리(중복조회·지도·정보수정) · 팀원 관리 · 프로젝트 참여자 · B02 등록 화면 · 대시보드 배치 | — |
| **1-2** | B01·B02 화면 다듬기 + **시스템 로그 기록 보강**(공용 기록 함수·대상 식별자·접속주소·보관 1년) | `017_audit_log_detail.sql` |
| **2-2** | 3D 구조물 선택이 B06 횡단 카드로 넘어가는 길 — **코드 결함 없음**. 안 되던 진짜 이유는 종단 오버레이가 클릭을 가로챈 것 | — |
| **2-3** | 종단 그래프 끌기 이동 — 종단 그래프·유토곡선·측점 테이블 **세 곳이 함께** 움직임 | — |
| **2-4** | 종단 테이블 12행 → **6행**, 높이 상한 204px 신설 (B07 도면 종단도는 12행 유지) | — |
| **2-5** | 유토곡선 초기 화면 정리(토량분배 숨김·전체 맞춤) + 최종 누가량을 종단 상단줄로 | — |
| **2-5c** | 유토곡선 자리 정리 — **총괄값 배지** 신설, B06 유토곡선 그래프 제거 | — |
| **2-6** | 구조물 측점 누가거리 정본 일원화 + **B06 종단 구조물 표시를 B05 와 같은 부품으로** 통일 | `67972612` |
| **2-7** | 초기값(초기 스냅샷) 실경로 검증 + `initial_design.lock` 흐름 확인 | — |
| **3-1** | 곡선부 노폭 **자동 확폭** — 법정 R 표 + 양끝 10m 테이퍼, 유효너비 5m 상한 | — |
| **3-2** | 구조물 절·성토 면적 반영 + **C군 벽(옹벽·돌쌓기·흙막이·큰돌쌓기)을 횡단 제원으로** 얹음. 구조물 저장이 통째로 유실되던 버그도 고침 | `6b76a3f2` |
| **3-4** | 기슭막이 연동을 풀면 **그 순간 높이로 굳게** 함(다단 포함) | `71694163` |
| **3-5** | 다단 벽이 원지반에서 뜨던 것 + **다단·벽 바깥 성토가 물량에 안 잡히던 것** 고침 (파급은 사용자가 「그대로 둠」으로 확정) | `e9ad8cdf` |
| **4-1** | 도각 **외부 파일 불러오기**(DWG→DXF→도각 JSON) + **자리표 배치 도구** | — |
| **4-2** | 도면 **DXF·DWG 내보내기** + LibreDWG 동봉(GPL 실행파일 호출) | — |
| **4-3** | 자리표 다듬기 — 칸(box) · 그림 비율 유지 · 편집 중 **실제 값 미리보기** | — |
| **4-4** | 자리표 후속 — 마우스로 칸 크기 조절 · 기본 도각 칸 정렬 · 도면명 미리보기 | — |
| **5-1** | 라이다 **다중 파일 입력·병합** + 씨닝(지면점은 전부 보존) + 먼 파일 차단 | — |
| **5-2** | 700줄 분리 뒤 B04 배수유역 결과가 분리 전과 같은지 확인 — **같음** | `07e6441d` |
| **5-3** | 「LAS 없이 설계」에서 PRJ·TFW 를 필수로 보던 오류 | — |
---
## 1. 공통 기반 — 노선 데이터와 계산 자리
노선이 무엇인지(데이터 모양)와 계산이 어디서 도는지를 정한 절들. 뒤의 모든 페이지가 이 위에 섬.
### 0-3. 노선 자동탐색 사용 중단 + 평면 법정 검사 (2026-09-06 사용자 확정)
지금 [최적 경로 계산]은 **평면만 보고** 노선을 정함. 실제 판단은 평면·종단·횡단·유토곡선을 함께 봐야 하므로 **현재 방식은 불필요**. 0-2 계획노선 편집이 그 자리를 대신함. 여기서 만드는 **측점별 평면 R 계산**은 3-1 확폭이 그대로 씀.
- [x] 좌측 「경로 계산 설정」의 [최적 경로 계산] 입구를 내림(코드는 남기되 화면에서 뺌).
- [x] 측점마다 평면 곡선반경 R을 구해 횡단 자료에 실음 — 횡단 생성이 이미 노선 폴리라인을 읽으므로 그 자리에서 계산(기존 외접원 반경 함수 재사용).
- [x] 평면 최소곡선반경 법정표(설계속도·지형별 40→60/40 · 30→30/20 · 20→15/12)는 **탐색 제약이 아니라 위반 표시** 쪽으로 씀 — 원청 노선·계획노선 모두에 적용.
- [x] 배향곡선(중심선 R 10m 미만)은 **경고만** 표시 — 자동 보정·차단 없음(2026-09-06 사용자 확정, 처리 방식은 뒤에 판단).
- [x] 내각 155° 이상 곡선 생략 — **0-10 에서 넣음**(별표2 Ⅰ.2.다, 폴리라인화 규칙의 일부).
이 줄의 옛 「보류」는 코드와 어긋나 여기서 닫음.
### 0-4. 계산 자리 일원화 — 브라우저에서만 도는 계산 없애기 (2026-09-06 사용자 지시)
**문제** — 사용자가 브라우저를 닫으면 계산이 안 돎. 지금 일부 값은 **브라우저가 만들어 서버에 저장만** 하므로, 그 화면을 한 번도 안 연 프로젝트에는 값이 없다. 초기 결과가 저장되려면 백엔드에서도 같은 계산이 돌아야 함.
**현황 조사 (2026-09-06)** — 계산이 어디서 도는지 네 갈래로 갈림.
| 갈래 | 무엇 | 지금 상태 |
|---|---|---|
| ⓐ 파이썬·TS **두 벌 짝** | 횡단 설계선·단면적 | `B06_Section_Engine_Design.py``common_util_cross_design.ts`, `_Engine_Areas.py``_design_areas.ts`. 값이 갈리지 않게 주석·테스트로 묶여 있음 |
| ⓑ **브라우저 TS 를 서버 Node 로 그대로 실행** | 3D 코리도 | `B05_Profile_Corridor_Node.ts`(SSR 번들) 를 `B05_Profile_Corridor_Prebuild.py` 가 전처리 체인 끝에서 호출해 영구저장. **계산을 다시 짜지 않는 유일한 선례** |
| ⓒ 브라우저 전담 + 서버는 **스냅샷 보관** | 유토곡선 | ~~프론트가 계산해 `mass_haul` 로 보내면 서버는 검산 없이 저장~~**해소됨**: 저장 때 서버가 Node 로 정본을 냄. 브라우저는 balloon 위치만 보냄(0-8 ⓕ) |
| ⓓ 브라우저 **전용**(서버에 없음) | 구조물 기하 — 기슭막이·집수정·다단, 세월교, BOX암거 / 그 위에서 낸 **구조물 폐회로 면적**(3-2) | 서버에는 옵션값만 있고 형상 계산이 없음. B07 도면에도 아직 횡단면도 엔진이 없어 드러나지 않았을 뿐 |
**방안 — ⓑ 방식으로 모은다 (계산은 한 벌, 실행 자리만 둘)**
파이썬으로 포팅하면 같은 기하가 두 벌이 되어 화면과 서버가 갈린다(코리도 때 이미 내린 결론). 대신 **브라우저가 쓰는 TS 를 Node 진입점으로 감싸 서버가 호출**한다.
- [x] 구조물 기하 + 폐회로 면적을 **Node 진입점 하나**로 묶음 — `B06_Section_Server_Calc_Node.ts`(입력 JSON → 출력 JSON, 코리도와 같은 꼴). 화면이 쓰던 「정본만 읽는 제어기 흉내」를 `B06_Section_Structure_Layouts.ts` 로 빼 B07 도면과 서버가 **같은 한 벌**을 씀.
- [x] 전처리 체인 끝(코리도 프리빌드 옆, **초기값 스냅샷 앞**)에서 한 번 돌려 영구저장 — `B03_FileInput_Service_Chain.py`.
- [x] [저장]·[확정] 때도 같은 진입점으로 서버가 다시 계산 — `B06_Section_Router_Confirm.py` 커밋 뒤 호출. 사용자 patch 가 먼저 들어간 뒤라 바뀐 벽 위치가 면적에 실림.
- [x] 유토곡선(ⓒ)도 같은 진입점에 넣음 — **면적 보정을 얹은 뒤** 쌓으므로 화면 순서와 같음. balloon 위치(사용자가 끌어 옮긴 화면값)는 서버가 만들지 않고 저장분에서 떼어 도로 붙임.
- [x] ⓐ 두 벌 짝은 **그대로 둠** — 이미 짝이 맞고 테스트가 지킨다. 새로 두 벌을 만들지 않는 것이 이 항목의 핵심.
- [x] 자체검증(2026-09-06 용화 신규 프로젝트) — 브라우저를 한 번도 안 열고 구조물 면적 15곳·유토곡선이 저장됨(측점 67곳 전부 면적 있음). 화면 유토곡선 절토 5,116.3㎥·성토 15,397.4㎥ = 저장값과 일치.
- [x] **[저장]·[확정] 은 서버가 정본을 계산함** (2026-09-06 저녁 사용자 최종 확정).
그날 낮에 「저장 때 Node 를 돌리지 말고 브라우저 계산으로」 잠깐 뒤집었다가 **되돌린
것**이니, 옛 커밋·주석에 그 흔적이 보이면 이 줄이 최신임.
이유는 속도가 아니라 **보안** — 저장 경로가 브라우저 계산이면 유토 배분·운반거리
코드가 번들에 남아야 해서 화면에서 안 그려도 못 뺌. 서버로 되돌려 그 몫이 빠졌음.
대가로 저장 대기가 늘었고(4.1초까지 갔다가 왕복 묶기로 **1.8~3.0초**), 기다리는
조작이라 허용. 실측·자리는 **0-8** 참조(옛 0-9 는 0-8 로 합쳐졌음).
**왜 포팅이 아니라 Node 실행인가** — 기하 계산은 화면 그리기와 같은 코드를 쓴다(트림·성토부선·벽 외곽). 파이썬으로 옮기면 화면이 바뀔 때마다 두 곳을 고쳐야 하고, 어긋나면 「그림은 이런데 수량은 저렇다」가 된다. 코리도에서 그 판단을 이미 했고 잘 돌고 있다.
### 0-10. 계획노선 폴리라인화 — 노선 데이터 모양 정정 (2026-09-06 사용자 확인)
**전제가 틀렸음** — 「예상노선 = 폴리라인」이라 보고 0-2·0-7 을 짰는데, 사용자가 실물을 보고
**예상노선은 점(포인트)으로 이뤄진 데이터**이며 규칙 없는 폴리라인과도 맞지 않는다고 확인함.
그래서 **계획노선은 원데이터를 복사한 뒤 폴리라인으로 바꾼 것**이어야 하고, 그것이
**불변의 초기 데이터**가 됨. 유토곡선·3D 에 투영되는 선도, 노드를 잡아 고치는 대상도 이것임.
**노선은 이제 세 벌** (옛 「두 벌」 서술은 0-2·0-7 에서 이 절을 참조할 것)
| 벌 | 자리 | 무엇 |
|---|---|---|
| 예상노선 | `B05_Profile/route/expected_route.csv` | 자동 체인이 낸 **점 묶음**. 불변 |
| **계획노선 초기본** | `planned_route_initial.csv` | 위를 폴리라인화한 것. **불변 초기 데이터** |
| 계획노선 | `planned_route.csv` | 사용자가 **노드**를 잡아 고친 수정본 |
읽는 순서(`load_design_route`) — 수정본 → **초기 폴리라인** → 예상노선 → 초기값 스냅샷.
[초기화] = 수정본을 지워 초기 폴리라인으로 돌아가는 것.
**곡선 기준은 지식DB** (`01_임도/02_상세설계/평면선형.md`, 별표2 .2.다 —
코드에서는 `config_system_design.FOREST_ROAD_PROFILE_CRITERIA["min_plan_radius_m"]`)
- 최소곡선반지름 — 설계속도 40: 일반 60 / 특수 40 · 30: 30 / 20 · 20: 15 / 12 (중심선)
- 배향곡선 중심선 R 10m 이상
- **내각 155° 이상**(교각 25° 이하)이면 곡선 생략 가능
- [x] **폴리라인화 모듈** `common_util/common_util_route_polyline.py` (커밋 `356b3216`)
-**꺾임점(IP)만 남김** — Douglas-Peucker, 허용오차는 격자 해상도(2m)의 두 배.
⚠ 이 단계가 **핵심**임. 예상노선은 격자 탐색이 낸 조밀한 점군이라(용화: 1,097m 에
331점 = 약 3.3m 간격) 점마다 곡선을 끼우면 접선 자리가 1.5m 뿐이라 **R 이 2~6m 로
뭉개짐**. 처음에 이 단계를 빼고 만들었다가 곡선 15곳이 전부 위반으로 나와 드러났음.
- ② 내각 155° 이상은 생략, 그 아래는 최소곡선반지름으로 원호를 끼움. 자리가 모자라면
반지름을 줄이되 **막지 않고 위반으로 표시**(사용자 확정: 자동 보정·차단 없이 경고만).
-**점을 옮기지 않음** — 노드는 원본 자리에 그대로 두고 그 사이에 원호를 넣음.
- `GET /route/plan` 이 노드(반지름·내각·위반)까지 함께 돌려줌.
`POST /route/replan` 도 받은 **노드**로 다시 폴리라인화 — 노드만 옮기면 선이 저절로
규칙을 지키게 하는 것이 이 구조의 목적임.
- 자체검증(용화, 실화면 API) — 예상노선 331점 → **편집 노드 25개 + 곡선 13곳**
(전부 R 12m = 설계속도 20·특수지형 법정 하한, **위반 0**) → 계획노선 정점 142개.
응답 180ms. 시험 `tmp/tests/test_route_polyline.py` 7건(원호가 접선과 맞물리는지 좌표 대조).
- [x] **모달이 노드를 그리고 잡게** (커밋 `9b93d305`) — 그려 보이는 선(폴리라인, 원호 포함)과
**잡는 점(노드)** 을 나눔. 노드에 붙은 반지름·내각·위반을 실었고 위반 노드는 붉게,
상태줄에 곡선 수·기준 R·미달 개수. 노드를 옮기면 폴리라인은 낡은 값이라 지우고
직선으로 미리 보임 — 곡선은 [확인] 때 서버가 같은 규칙으로 다시 끼움(계산 한 벌).
자체검증 — 모달이 **노드 28개**로 열리고 「노드 28개 · 초기 폴리라인 · 곡선 13곳(R 12m)」.
전에는 폴리라인 정점 142개를 잡았음.
- [x] **완만한 긴 굴곡이 삼켜지던 것** (커밋 `00decc81`) — 노드 사이가 **100m** 를 넘으면
그 구간만 허용오차를 절반으로 낮춰 다시 뽑음(최대 3겹). shapely 대신 색인을 돌려주는
Douglas-Peucker 를 직접 둠(되뽑기가 원본 구간을 꺼내야 함). 새 점은 안 만듦.
- 자체검증(보조 창, 되뽑기 전후 대조) — 1.1km 둘 노드 25→28 · 최대 간격 **82m**(상한 안) ·
위반 0. 4.5km 노드 20→24, 최대 간격 1,300m 는 **실제 직선**(그 구간 내각 최소 158°,
원본 점 간격 중앙 3.3m·최대 3m 로 고름).
-**되뽑기가 곡선 유무를 바꿀 수 있음** — 4.5km 노선에서 노드 자리가 옮겨지며 정확히
155° 였던 두 곳이 155° 위로 올라가 곡선 2 → 0 이 됐음. 별표2 「155° 이상 생략 가능」에
맞으므로 문제는 아니나 알아 둘 것.
- [x] 🔴 **노드를 옮기고 [확인] 하면 재계산이 중간에 끊김** (2026-09-06 보조 창 실측)
→ 아래 네 갈래 전부 닫힘. **남은 것은 시간뿐** — 끝까지 도는 값이 208~221초라
「0-11. 노선 [확인] 이 3분 반」 으로 옮겨 따로 다룸.
- 증상 — `route/replan``success` 를 돌려주고 화면도 성공으로 보이는데, 체인이
**B05 재계산에서 400 으로 중단**돼 그 뒤(종횡단 재생성·확정)가 통째로 안 돎.
재계산이 **2.1초**에 끝난 것으로 먼저 드러남(코리도 사전 생성 하나만 4.3초).
- 결과가 **어긋난 채 굳음**`route/plan` 1,079.4m · `pipe-points` route_length 1,079.44
(둘 다 새 노선)인데 `sections/detail` 은 측점 67·끝 1,097.4m(옛 노선). 관 누가거리가
새 노선 기준이라 어느 측점과도 안 맞아 **배수관 측점이 9 → 0** 이 됐음.
- 자리 — `B03_FileInput_Service_Chain.py``solve_route(project_id, request)`
`JSONResponse(400)` 를 돌려주고 그 아래에서 중단 로그만 찍고 끝남.
- [x] **사유가 안 남던 것부터 고침**(커밋 `eec29a90`) — 상태 코드만 남겨 무엇이 막았는지
못 짚었음. 이제 본문(message)까지 로그에 남음. 보조 창이 재현해 사유를 확인 중.
- **사유 확정**(보조 창 재현) — 「세그먼트 1 (BP → CP1) 경로 탐색 실패: 종단경사
한계(26%)·최소곡선반지름(12m)·회피지역 제약으로 통과 경로가 없습니다」. 자동탐색이
사용자 노선을 **다시 푸는 중** 막힌 것.
- [x] **곁가지 둘 먼저 고침**(커밋 `53525a1d`) — ① 실패해도 계획노선 CSV 는 이미 바뀐
뒤라 누를 때마다 어긋남이 쌓였음(1,097.4 → 1,079.4 → 1,064.7m). 쓰기 전에 이전 수정본을
쥐고 있다가 실패하면 되돌림. ② `route/replan``success` 를 돌려줘 화면이 성공으로
보였음 — **409 + 사유**로 응답. `run_redesign_chain` 이 중단 사유를 돌려주게 함.
재현 확인(보조 창) — 같은 조작에 409 + 원래 사유, 노선은 1,064.7m 그대로.
- [x] **근본 원인 고침**(커밋 `674ca827`) — `B05_Profile_Engine_AsPlanned.solve_as_planned`
신설. **제어점 목록이 곧 노선**이고 표고만 지표면 격자에서 뜸. 반환 꼴을 솔버와 맞춰
아래 단계가 그대로 이어짐. 위반은 세되 막지 않음. `run_route_design`
`algorithm="as_planned"` 갈래를 내고 재확정 체인이 그것을 씀 — **자동탐색은 이제 초기
업로드에서만** 돎(0-3 방향과 맞음). 솔버와 지표 계산이 겹치지만 합치지 않음(솔버는
0-3 으로 접히는 코드).
- [x] 실화면 재검증(보조 창) — ⓐ 노선 길이 바뀜 ✅ ⓑ **측점 67·1,097.4m 고정 →
64·1,079.4m**(노선과 일치) ✅ ⓒ **배수관 측점 0 → 7~9** ✅. 「세그먼트 경로 탐색 실패」
로그가 사라졌고 재계산이 2.2초(실패) → **208~221초**(끝까지 도는 값)가 됐음.
- [x] **노선 변경 때 옛 사용자 설계가 덮여 사라지던 것** (커밋 `908c75d5`) — 다단 구간값
(`extra_spans`)이 재계산 뒤 0건이 됐음. **이월은 돌고 있었는데 곧바로 덮였음**
`_prepare_drainage_pipes_and_reprofile(refresh=True)` 가 측점을 새 노선 기준으로 다시
만들며 방금 얹은 설계를 갈아 끼웠고, 이월이 그 **앞**에 있었음.
이월을 측점 재생성 **뒤**로 옮기고 `merge_cross_section_designs` 로 묶음.
보조 창이 재검증 중.
- [x] **최소곡선반지름 조회가 없는 컬럼을 읽던 것** (커밋 `eec29a90`) — `projects` 에는
`road_type` 뿐이고 `design_speed_kph`·`terrain_type` 은 없음. 매 요청마다
`OperationalError(1054)` 가 ERROR 로 남고 기본값 폴백으로 넘어갔음(값은 우연히 맞았음).
임도 종류는 `projects.road_type`, 설계속도·지형은 **워크플로 stage 2 params** 에서 읽게 함.
산식은 이미 있던 `B05_Profile_Engine_Grade.legal_plan_radius_min_m` 을 씀 —
내가 같은 표를 다시 짜 뒀던 것을 지움.
- [x] **초기 업로드부터 노선 정본이 폴리라인이 되게** (커밋 `41852da3`) — 폴리라인 파일만
만들고 정작 **노선은 원본 점군으로 풀고 있었음**. 그래서 첫 업로드 뒤 종횡단 측점·
유토곡선·3D 코리도가 **곡선 없는 점군** 위에 섰고, 노선을 한 번이라도 편집한
프로젝트만 우연히 폴리라인 기준이었음.
실측(용화 3건) — DB 노선 정점 **331·323 vs 초기 폴리라인 145**. 편집한 하나만 145.
자동 체인에 2.7 단계를 넣어 초기 폴리라인을 세운 **뒤 노선을 다시 읽어** 그것을
BP·경유점·EP 로 삼고 `algorithm="as_planned"` 로 넘김(재확정 체인과 같은 길).
자체검증 ① 2.7 재판독이 **145 정점**을 돌려주고 BP·EP 좌표가 `expected_route.csv`
첫·끝 줄과 **완전 일치**. ② 진짜 `solve_route` 를 그 입력으로 돌려 **노선 연장 1,079.5m ·
저장 정점 145** 를 얻음 — 사용자가 노드를 고쳐 만든 b269ea34 의 **1,079.4m·145**
같은 값이 **다른 경로로** 나옴. ③ `pytest` 409 passed.
⚠ 이미 만든 프로젝트는 그대로임 — 새로 업로드해야 폴리라인 기준이 됨.
**검증 함정**`route_main.geojson` 은 **프로젝트당 한 벌**이라 `solve_route`
한 번만 불러도 살아 있는 노선의 파일을 덮음(노선 행은 늘어도 파일은 하나).
위 ②에서 실제로 덮였고, 예상노선 331점을 `solve_as_planned` 로 다시 떠
**331 정점·1,097.4m**(노선 행 값과 일치)로 되돌렸음. 살아 있는 프로젝트에
검증용 solve 를 돌리지 말 것.
- [x] 🔴 **[확인]을 누를 때마다 계획노선이 깎이던 것** (커밋 `19444149`, 2026-09-07) —
보조 창이 재측정 중 발견. 같은 노선으로 [확인]을 되풀이하면 정점이 **136 → 134 → 119**,
노드가 **22 → 21** 로 계속 줄었음. 사용자가 아무것도 안 옮겨도 누를 때마다 자기 노선이
뭉개졌음.
- 원인 — **이미 폴리라인인 것을 다시 단순화**. 두 자리 모두 그랬음:
`read_route_plan` 이 노드를 계획노선(원호 점이 섞인 폴리라인)에서 되뽑음
`replan_route` 가 화면이 보낸 노드를 또 한 번 단순화해서 씀.
단순화(Douglas-Peucker)는 **원본 점군에서 한 번만** 돌아야 함.
- 고친 것 — `build_planned_polyline``simplify` 갈래 신설(False 면 받은 점을 꺾임점으로
그대로 씀) · 폴리라인을 쓸 때 **그것을 낳은 노드도 함께 저장**
(`planned_route_nodes.csv` · `planned_route_initial_nodes.csv`) · `read_route_plan`
저장된 노드를 그대로 씀(없는 옛 프로젝트만 뽑음) · `replan_route``simplify=False`.
- 실측(용화 b269ea34 실제 노선 파일, 4회 왕복) —
옛 방식 `노드21/정점115 → 20/115 → 20/113 → 20/115`(깎이고 흔들림),
고친 방식 `노드21/정점115`**매 회차 완전히 동일**.
시험 `tmp/tests/test_route_polyline_idempotent.py` 2건. `pytest` 414 passed.
- ⚠ 이미 고쳐 둔 프로젝트는 첫 조회에서 한 번만 자리를 잡음(119 → 115) — 노드 파일이
없어 폴리라인에서 한 번 뽑기 때문임. 그 뒤로는 안 움직임.
### 0-12. 계획노선을 「직선 → 곡선 → 직선」으로 (2026-09-07 사용자 확정)
**사용자 확정 원문** — 「계획노선은 직선>곡선>직선 형태의 폴리라인임. 여기서 반지름은 법정
최소 값을 지키되 **기존 예정노선에 가까운 폴리라인을 찾는 게 키**임. 그 후 사용자가 직선과
곡선의 교차점을 이동하고 필요한 경우 곡선 값을 변경. 단 **곡선 반지름값 변경 시 주변 직선
각도 변경 구속**. **직선과 곡선 교차점 이동 시에는 직선의 각도와 반지름 값 변경**이 되.」
**되물어 확정한 것 둘 (2026-09-07)**
- **끄는 점 = 곡선 시작·끝점**(직선이 곡선에 닿는 자리, BC·EC). 꺾임점(교각점)이 아님.
- **반지름은 법정 최소 이상에서 자유** — 「원본에 가장 가까운 값」으로 고름. 상한은 안 둠.
**지금과 무엇이 다른가**
| | 지금 | 바꿀 것 |
|---|---|---|
| 반지름 | 모든 곡선이 **법정 하한 고정**(R 12m) | 구간마다 **예정노선과 가장 덜 벌어지는 R**(≥ 하한) |
| 잡는 점 | 꺾임점(노드) | **곡선 시작·끝점** |
| 꺾임점을 옮기면 | 양쪽 각도·R 다시 계산 | (그대로 두되 손잡이는 곡선 끝점) |
| R 을 바꾸면 | 없음 | **주변 직선 각도 고정** — 접선점만 미끄러짐 |
- [x] **① 반지름 피팅** (커밋 `db700496`) — **꺾임점 하나마다 원호를 끼우는 방식으로는
안 되는 것**이 실측으로 드러남. R 을 키울수록 오히려 원본에서 멀어졌음(평균 1.56 →
1.89m). 한 곡선은 한 원호여야 하므로 **이어진 꺾임을 묶는 것**이 먼저였음.
- 고친 것 셋 — ⓐ 같은 쪽으로 도는 꺾임을 한 곡선으로 묶고 앞뒤 직선을 늘려 만나는 자리를
교각점으로 씀(`_curve_runs`) ⓑ 그 안에서 법정 하한 이상·예정노선과 평균 벗어남이 가장
작은 R 을 고름(`_fit_radius_m`) ⓒ 묶는 것이 손해면 도로 쪼갬(`_split_wide_runs`
평균이 나빠지거나 한 자리라도 8m 넘게 벌어지면).
- **별표2 의 155° 를 「묶은 뒤 전체 교각」에 적용**하도록 자리를 옮김. 조각마다 적용하면
완만한 곡선이 쪼개졌음(내각 156·138·154° 세 꺾임이 갈려 가운데 하나만 곡선).
쪼갤 때도 다시 검사함 — 안 하면 펴진 자리(내각 159°)에 곡선이 생겼음.
- 실측(흉내낸 매끄러운 곡선) — 원본 **R 25 → 25.1m · 40 → 39.8m · 80 → 78.2m** 로 되찾음
(벗어남 0.09~0.44m). 전에는 전부 R 12m 로 뭉개져 1.3~2.7m 벌어졌음.
- 실측(용화 실제 노선) — **중립**. 노드 28 · 곡선 13 · 정점 145 · 위반 0 · 벗어남 최대
3.57m 평균 1.077 → 1.081m. 반지름만 12~28m 로 갈림. 격자 탐색이 낸 지그재그라 급한
꺾임이 대부분이고, 그런 자리는 하한이 이미 최선이라 그럼.
- 시험 `tmp/tests/test_route_polyline_curve_fit.py` 4건.
- [x] **② 직선·곡선 성분을 정본에 남김** (커밋 `e593378d`) — `RouteCurve` 신설(교각점·
반지름·접선길이·내각·곡선 시작점·끝점·대신하는 꺾임점 구간·위반).
`planned_route_curves.json` 으로 저장하고 `GET /route/plan``curves` 로 내려보냄.
**반지름을 바꿔도 교각점은 안 움직임** — 그것이 「주변 직선 각도 구속」의 뜻임.
- [x] **곡선 생략 규칙을 쓰지 않음** (커밋 `9626d9e8`, 2026-09-07 사용자 지시) —
「내각 155 이상이면 곡선 생략은 반영하지 말자. 사용자가 계획 평면 노선을 수정할 때
문제가 될 것 같아.」 곡선이 있다 없다 하면 잡는 손잡이도 있다 없다 하고, 조금 폈다는
이유로 곡선이 사라지면 되돌릴 길이 없음. 이제 **꺾이는 자리에는 늘 곡선**.
실측(용화) — 곡선 13 → 26곳, 정점 145 → 181, R 12~28 → 12~199m, 내각 109~177°.
벗어남은 그대로(최대 3.57m · 평균 1.081 → 1.102m).
**평면 R 을 읽는 자리(확폭 3-1, 구조물 2-6)는 곡선 개수가 늘어난 것을 감안할 것.**
- [x] **곡선·직선 지우고 더하기 + 반지름 못박기** (커밋 `f59932de`, 사용자 지시
「r과 직선 삭제나 추가가 있어야 하지 않을까?」) — 서버 쪽 완료.
편집을 **꺾임점 목록 + 자리마다 곡선 켬끔 + 반지름** 셋으로 표현함:
직선 삭제·추가 = 점을 빼거나 더하기 · 곡선 삭제·추가 = `curve` 끄고 켜기 ·
반지름 변경 = `radius_m` 못박기. `POST /route/replan` 이 정점마다 그 둘을 받음.
편집값이 오면 **묶지 않음**(사용자가 곡선 하나로 본 것을 임의로 합치면 손잡이가 사라짐).
시험 `tmp/tests/test_route_polyline_edit.py` 7건 — 그중 「반지름을 바꿔도 교각점은
안 움직인다」가 사용자가 말한 **「주변 직선 각도 구속」** 을 못박음.
- [x] **④ 화면에 곡선 제어 붙임** (커밋 `56a6d186` + `43d55f49`) —
곡선 시작·끝점을 **네모로 표시**(곡선 범위를 눈으로 봄) · 꺾임점을 누르면 고른 자리가
되고 아래 **편집줄**이 그 곡선을 만짐 · **R 입력칸**(값 넣으면 못박고 [반지름 자동]으로
되돌림) · **[곡선 지우기]/[곡선 넣기]**(지운 자리는 가운데 빈 동그라미) ·
[확인]이 정점마다 `curve`·`radius_m` 을 함께 보냄.
노드 삽입·삭제 때 **편집값 목록도 같은 자리에서 밀어 줌** — 안 그러면 뒤 노드의
R·켬끔이 한 칸씩 어긋남.
- 실화면 확인(용화, 20번째 꺾임점) — 고름 「자동 · 내각 155° · 법정 하한 12m」 →
곡선끔 「곡선 없음 — 직선이 그대로 꺾입니다」(상태줄 「곡선 지움 1곳」) → 곡선켬 「자동」 →
R 40 「값 지정」(「R 지정 1곳」) → 자동 되돌림까지 **넷 다 동작**.
- ⚠ 함정 — 처음엔 단추가 **캔버스 밑에 깔려** 클릭이 가로채였음(`intercepts pointer
events`). 세로 flex 에서 캔버스가 `flex:1` 이라 새 줄이 눌린 것. 바닥 단추줄처럼
`flex: none` + `z-index: 1` 을 줘야 함.
- [x] **③ 곡선 시작·끝점을 끌면 직선 각도와 R 이 함께 바뀜** (커밋 `20f34390`) —
셈은 **서버의 반대 방향**임(서버: 교각점·R → 접선점 / 화면: 끈 접선점 → 교각점·R).
끈 쪽 직선만 돌리고 반대쪽은 옛 교각점을 지나는 그대로 두어, 두 직선이 만나는 자리가
새 교각점이 되고 `R = |새 교각점 끈 자리| / tan(교각/2)`.
- **두 벌이 아니라 짝** — `tmp/tests/test_route_polyline_handle_drag.py` 3건이 왕복을 지킴.
화면이 낸 (교각점, R) 을 서버 셈에 넣으면 끈 자리가 **1µm 안**으로 되돌아옴.
접선점을 직선 위에서만 밀면 교각점이 안 움직이고 R 만 커지는 것도 확인.
- 손잡이를 **속 빈 네모(5px)** 로 키움 — 3.5px 채운 네모는 선·노드와 색이 같아 눈에도 안
띄고 집기도 어려웠음. 곡선을 지운 자리에는 손잡이도 없음.
- 실화면 확인 — 손잡이를 끌자 「14번째 꺾임점 · R 16.6 · 값 지정」. 서버가 곡선 성분
22개(예: R 44.3m, 노드 1~3을 한 곡선으로)를 내려줌.
- ⚠ **또 옛 코드로 도는 서버에 속았음**(오늘 두 번째) — 응답에 `curves` 키 자체가 없었는데
상태줄은 「곡선 15곳」으로 보여 정상처럼 읽혔음(다른 값으로 세는 폴백이 있었음).
**화면 검증 전에 응답에 새 필드가 실제로 있는지부터 볼 것.**
- [x] **⑤ 법정 하한 미달은 막지 않고 붉게 표시** — 기존 규칙 그대로 섬. 위반이 붙은 자리는
모달에서 붉은 동그라미로 보이고, 곡선을 지운 자리는 가운데가 빈 동그라미로 구분됨.
- [x] 자체검증 — 예상노선과의 벗어남을 피팅 전후로 수치 대조했고(위 ① 참조), 편집 왕복이
제자리인지도 시험 셋으로 못박음: `test_route_polyline_idempotent`(깎임 없음) ·
`test_route_polyline_handle_drag`(손잡이 왕복 1µm) ·
`test_route_polyline_apex_roundtrip`(교각점으로 갈아 끼워도 선이 1mm 안에서 같음).
- [x] **한 번 편집하면 반지름이 하한으로 눌리던 것 고침** (커밋 `a0bd9c87`) —
실화면에서 잡음. 곡선 하나만 지워도 나머지가 **R 12~199m → 전부 12m** 로 되돌아갔음.
편집은 「꺾임점 하나 = 곡선 하나」인데 서버가 묶은 곡선의 교각점은 원본 꺾임점 중
어느 것도 아니라, 손대는 순간 묶음이 흩어져 각 자리가 하한으로 떨어졌음.
**모달이 열 때 묶인 구간을 그 교각점 하나로 갈아 끼우게** 함 — 그려지는 선은 똑같고
이제 손대도 안 흩어짐. 실화면 확인: 되돌린 뒤 곡선 26곳(R 최대 198.9m) → 곡선 하나
지우고 [확인] → 25곳, **R 42.5·51·198.9m 그대로**.
- **[확인] 한 번 = 약 90초** (오늘 아침 293초에서 줄어든 값. 0-11 참조)
- [x] **곡선 반지름을 노드마다 손대게 할지** — 사용자 확정: **손대게 함**. 위 0-12 로 옮김.
사용자가 특정 곡선만 크게 하고 싶을 수 있음(0-2 의 「곡선(R) 성분 편집」과 같은 항목).
- [x] **초기 폴리라인을 자동 체인에서 세움**(커밋 `fe14ad02`) — 확인(보조 창): 파일을 지우고
`_ensure_initial_polyline` 을 부르면 **794ms 에 재생성, 옛 파일과 바이트 동일**.
브라우저를 한 번도 안 열고 생김.
- [x] 단순화 허용오차 확인 — 표본 5개(1.1km 셋 · 4.5km 하나 · 노선 없는 둘)로
**4m + 노드 간격 상한 100m** 로 닫음.
---
## 2. B01·B02 — 대시보드·프로젝트 등록
### 1-1. B01 대시보드 권한·회사·프로젝트 연결 정비 (2026-09-06 사용자 협의)
**확정** — ① 시스템 관리 회사는 개발사 자기 회사 한 곳뿐이며 새로 만들 일 없음. 그 회사 소속이면 역할을 고를 것 없이 전원 시스템관리자 ② 고객사는 회사 생성자가 회사관리자, 나머지는 일반사용자 ③ 타사 사용자 공유는 보류 ④ 도면에는 한 사람 이름만 나가되 설계 과정의 보조 인원은 여러 명일 수 있어 **참여자 목록**을 따로 둠.
#### 1) 시스템 회사 자동 권한
시스템 회사 = `.env` 의 관리자 계정이 소속된 회사(현재 `캐미팩토리`). 표시 칸이나 설정 화면은 만들지 않음.
- [x] 서버 시작 때 그 회사 소속 전원을 시스템관리자로 맞춤 (`main.py` 의 관리자 이메일 한 줄 옆).
- [x] 가입 승인·팀원 등록으로 그 회사에 들어오면 시스템관리자 부여, 다른 회사는 일반사용자 (`B01_Dashboard_Repository.py` `process_join_request`, `_Repository_Members.py` 팀원 등록).
- [x] 회사에서 빠지면 일반사용자로 복귀 — 현행 그대로임을 확인만.
- [x] 견본 계정·역할 칸은 그대로 둠 (2026-09-06 사용자 확정).
- [x] 자체검증 — 소속 4명의 역할 변화를 DB 로 확인.
#### 2) 회사 관리
- [x] 회사 생성 모달에 **중복 조회** — 사업자등록번호·회사명으로 기존 회사를 찾아 보이고, 있으면 「가입 신청」으로 유도.
- [x] 입력 순서 = 사업자등록번호 > 회사명 > 대표자명 > 회사 로고 > 주소.
- [x] **주소 입력 + 지도 미리보기** — 이미 쓰는 VWorld 키를 서버 경유로 재사용(새 지도 서비스 도입 없음).
- [x] **회사 정보 수정 화면·API 신설** — 회사관리자는 자기 회사, 시스템관리자는 전체.
- [x] 자체검증 — 같은 사업자번호로 생성 시도 시 안내가 뜨는지, 수정값이 도면 표제란·로고에 반영되는지 실화면 확인.
#### 3) 사용자(팀원) 관리
- [x] 「팀원 추가」를 **계정 대신 만들기 → 소속 없는 기존 가입자 검색·선택** 으로 교체. 다른 회사 소속자는 목록에서 제외. (지금은 임의 비밀번호로 계정을 만들어 당사자가 로그인 못 함)
- [x] 아직 가입 안 한 사람은 **안내 메일만** 발송.
- [x] 입력 순서 = 이름 > 직급 > 이메일 > 부서.
- [x] 기본정보 폼과 사용자 수정 폼을 **한 벌로 통일** (배치는 조절 가능).
- [x] 회사관리자에게 **역할 변경** 허용 — 자기 회사 한정, 본인 강등 포함. 마지막 회사관리자 이탈 방지(`count_company_admins`)는 유지, 시스템관리자는 전역 제어.
- [x] **비활성화·삭제** 구현 — 회사관리자도 자기 회사 팀원 삭제까지 가능, 시스템관리자는 전역.
- [x] **가입요청 반복 신청 방어** — 이미 소속이 있는 사용자가 「회사 찾기」로 재신청하면 지금은 소속이 풀리고 승인대기로 떨어짐(`B01_Dashboard_Repository.py` `join_company`). 소속자에게는 신청 자체를 막을 것.
- [x] 가입 요청 목록과 사용자 관리 목록의 중복 표시 정리.
- [x] 자체검증 — 소속자가 재신청해도 소속 유지, 마지막 관리자 강등 거부, 권한 밖 호출은 백엔드가 403 인지 확인.
#### 4) 프로젝트 권한·담당자
- [x] 일반사용자도 **회사 프로젝트 전체 열람**(수정 불가) — 지금은 자기가 만든 것만 보임(`list_user_projects`).
- [x] 회사관리자에게 **자기 회사 프로젝트 삭제** 허용 (지금은 시스템관리자만).
- [x] **참여자 표 신설** — 프로젝트에 여러 명을 붙임. 만든 사람이 기본 참여자.
- [x] 참여자면 일반사용자도 그 프로젝트 수정 가능 — 백엔드 `_can_edit_project` 와 프론트 헬퍼를 같이 고침.
- [x] 도면 표제란 3역할(과업책임자·분야별책임자·설계자)은 각 1명 유지.
- [x] 자체검증 — 일반사용자 계정으로 남의 프로젝트 열람 가능·수정 403, 참여자로 넣은 뒤 수정 성공을 실화면으로 확인.
#### 5) 프로젝트 생성 화면 (B02)
- [x] **취소 버튼** 추가 — 대시보드로 복귀.
- [x] **프로젝트명 자동 조합** — 사업연도 + 사업지역 + 임도종류 + 수기 입력값을 한 줄로 만들어 그대로 저장.
- [x] **예상 연장 칸 삭제** — 대신 「노선 종료 누가거리(m)」 제목에 (종료 − 시작) 길이를 표기.
- [x] 「연도 기번(표지)」·「사업량(표지)」 입력칸 삭제 — 위 두 값과 중복. 도면 표제란·표지는 프로젝트명·노선 연장에서 끌어 쓰게 잇고 B07 출력 영향 확인.
- [x] 담당자 선택의 「+ 신규 등록…」 제거 — 계정 등록 후 고르는 길만 남김.
- [x] 담당자 기본값 = 만든 사람.
- [x] 자체검증 — 저장된 이름·연장·표제란 값이 도면에 그대로 나오는지 실화면 확인.
#### 6) 대시보드 배치·문구
- [x] 「시스템 로그」를 **맨 아래로** — 지금은 기본정보·보안 칸이 그 뒤에 붙어 있음.
- [x] 서명 고르는 칸 문구 정리 — 「○○에 물리기」·「올리고 선택」이 무슨 뜻인지 읽히게.
- [x] 자체검증 — 실화면 스크린샷으로 순서·문구 확인.
### 1-2. B01·B02 후속 — 화면 다듬기 (2026-09-06 사용자 지시)
실화면을 훑어 나온 것들을 그 자리에서 고침. 다섯 건 모두 공용 브라우저(8001·5174)에서 수치·스크린샷으로 확인.
**B02 프로젝트 등록**
- [x] **프로젝트명을 고칠 수 있는 칸으로** — 「저장될 이름」 미리보기 줄을 없애고 입력 칸 하나로 통합. 사업연도·사업지역·임도종류로 자동으로 차되 사용자가 사이나 끝에 글을 넣거나 자동 글자를 고칠 수 있음.
- [x] **자동 조합이 끊기던 것 고침** — 문자열을 찾아 바꾸던 방식이라 자동 글자 사이에 한 글자만 끼워 넣어도 연동이 통째로 끊겼음(실측: 「울진군 금강송면」의 「금강송면」 앞 삽입). 글자마다 주인을 기억하는 방식(`B02_ProjRegister_UI_Name.ts`)으로 다시 만듦.
- [x] 회사 로고 칸 삭제 — 프로젝트가 이미 회사에 매여 있고, 표제란은 `COALESCE(프로젝트, 회사)` 로 회사 로고를 씀.
- [x] 비고를 맨 마지막 항목으로 이동.
- [x] 제목·버튼 줄을 공용 템플릿(`createGeneralLayout`·`ui-general-block__actions`)으로 전환, 화면 폭은 종전 720px 유지.
**B01 대시보드**
- [x] **진행도(%) 열 삭제** — 워크플로 배지가 같은 것을 보여 줌. 상태 문자열로 따로 세던 서버 계산도 제거(배지는 `project_workflow_stages` 표가 근거라 둘이 어긋나 「0% 인데 횡단설계까지 켜짐」이 나왔음).
- [x] **시스템 로그 표 정비** — `이메일 · 동작 · 대상 · 일시`. 관리 버튼 열과 같은 문구(「관리」)를 돌려 쓰던 것을 가르고, 대상(`resource_type`·`resource_id`)을 새로 보이며, 일시는 날짜/시각 두 줄(아랫줄 작은 글씨라 행 높이 불변).
- [x] **표 관리 버튼 한 줄** — 작은 버튼 + 줄바꿈 금지, 폭이 모자라면 표가 가로 스크롤. 프로젝트 행 105px → **61px**, 사용자 행 55px.
- [x] **기본정보 = 로그인 본인 화면** — 이메일 라벨을 「팀원 이메일」에서 「이메일」로, **본인 서명 칸**을 사용자 수정 모달과 같은 부품으로 추가.
- [x] 제목·여백을 공용 템플릿으로 통일(역할 배지는 제목 줄 오른쪽), 헤더의 사용자명·로그아웃 버튼이 붙어 보이던 것 간격 부여.
**시스템 로그에 더 낼 수 있는 것 (2026-09-06 조사)**
- 대상 번호(`resource_id`)는 칸이 있으나 **기록할 때 `NULL` 로 넣는 자리가 많음** — 번호까지 보이려면 기록 쪽을 고쳐야 함.
- IP·브라우저(`ip_address`·`user_agent`)는 표에 칸만 있고 **기록을 안 함**(INSERT 에서 빠짐). 보안 로그로 쓰려면 기록부터 붙일 것.
**시스템 로그 기록 보강 (2026-09-06 사용자 확정 — 완료)**
- [x] 기록을 공용 함수 `common_util_audit.record_audit()` 한 곳으로 모음 — 여섯 자리에 흩어져 있던 raw INSERT 제거.
- [x] **대상 식별자 칸 신설** (`db_management/017_audit_log_detail.sql`, 적용 완료) — 옛 `resource_id` 는 INT 라 프로젝트 UUID 를 못 담아 늘 비어 있었음. 프로젝트 생성·수정·삭제, 회사 생성이 대상을 남김.
- [x] **접속 주소·브라우저 기록** — 라우터가 요청을 넘기고, 프록시 뒤에서는 `X-Forwarded-For` 첫 주소를 씀.
- [x] **보관 기간 1년** (`AUDIT_LOG_RETENTION_DAYS` 기본 365일) — 임시 보관함 정리 루프에 얹어 함께 지움. 종전에는 지우는 코드가 아예 없어 무한히 쌓였음.
- [x] 자체검증 — 실화면에서 프로젝트 생성·하드삭제를 돌려 로그 두 줄이 `프로젝트 e1040640… · 127.0.0.1 · 09:54 / 09:55` 로 남는 것 확인. 시험 `tmp/tests/test_common_util_audit.py` 6건.
#### 자체검증 (2026-09-06, 보조 워크트리 8001·5174 공용 브라우저)
> 계획서에서는 이 기록이 「## 2. B05 종단설계」 머리에 놓여 있었으나, 내용은 전부
> **B01·B02**(권한·회사·팀원·프로젝트 등록) 검증이라 여기로 옮겼음.
- 시스템 회사 자동 권한 — 서버 재시작 뒤 DB 확인: 캐미팩토리 소속 4명 전원 `SYSTEM_ADMIN`(견본 계정 포함, 사용자 확정대로 예외 없음).
- 대시보드 순서 — 실화면 섹션이 `리소스 현황 · 프로젝트 · 임시 보관함 · 사용자 관리 · 가입 요청 · 회사 관리 · 기본정보 · 보안 · 시스템 로그` 로, 시스템 로그가 맨 아래.
- 회사 정보 수정 — 회사 관리 표의 「수정」에서 모달이 뜨고 칸 순서가 사업자등록번호 · 회사명 · 대표자명 · 회사 로고 · 주소.
- 주소 지도 — 「강원특별자치도 춘천시 중앙로 1」 입력 후 「지도 확인」: 타일 9장 모두 로드, 표식이 강원도청 자리(스크린샷 `tmp/browser/shots/1205_map.png`).
- 팀원 등록 — 「엄상」 검색 결과 없음(소속 있는 사람이 제외됨을 확인), 한 글자면 「두 글자 이상」 안내. 「가입 안내 메일 보내기」 단추 존재.
- 사용자 관리 행 동작 — 수정 · 역할 변경 · 사용자 삭제 3종 노출.
- 가입 재신청 방어 — 소속 있는 계정으로 `POST /api/dashboard/user/company/join` 호출 시 **409**, 이후 `/me` 의 `company_id`·`status` 그대로(예전에는 소속이 풀렸음).
- 프로젝트 참여자 — 「도엽등고 테스트」 수정 모달에 생성자(엄상돈)가 체크된 상태로 뜨고, 박동철을 더해 저장 → DB `project_members` 2행 확인.
- 참여자만 바꿔 저장하면 「프로젝트를 찾을 수 없습니다」로 튕기던 문제(값이 하나도 안 바뀌면 rowcount 0)를 고침 — 재시도 시 「저장되었습니다」.
- B02 등록 화면 — 예상 연장·연도 기번·사업량 칸 사라짐, 취소 단추 생김, 담당자 3칸 기본값이 만든 사람(엄상돈), 프로젝트명 미리보기 `2026 정선군 간선임도 가리왕산지구`, 종료 칸 제목 `노선 종료 누가거리 (m) — 연장 2140.0m`(2260120).
- 테스트 — `./venv/Scripts/python.exe -m pytest tmp/tests/ -q` **186 passed, 1 skipped**. 새 테스트 `tmp/tests/test_b01_permissions.py` 가 참여자 수정권한·시스템 회사 역할 규칙을 확인.
- 미검증 — 일반사용자·회사관리자 계정의 실사용 확인은 DB에 시스템 회사 하나뿐이라 못 함(고객사 계정이 생기면 확인 필요). 마지막 관리자 강등·삭제 차단도 같은 이유로 코드 경로만 확인.
- **고객사 흐름 실검증 (2026-09-06 추가)** — 임시 회사 「검증용 임시사」와 계정 3개(회사관리자·일반사용자·무소속)를 만들어 실제 로그인(OTP 메일 수신)으로 확인한 뒤 **전부 삭제**함. 결과: ① 회사관리자 화면은 자기 회사 프로젝트만 보이고 수정·삭제 둘 다 됨 ② 일반사용자는 회사 프로젝트 2건을 모두 보되 참여자인 1건에만 「수정」이 뜨고, 남의 프로젝트 수정·삭제·팀원등록·회사수정·사용자삭제는 백엔드가 403 ③ 마지막 관리자 강등 409, 본인 계정 삭제 409, 다른 관리자를 세운 뒤에는 강등 200 ④ 남의 회사 정보 수정 403, 다른 회사 소속자를 팀원으로 붙이기 409 ⑤ 팀원 빼기 → 후보 목록 등장 → 다시 붙이기 성공 ⑥ 무소속 계정의 가입 신청 → 관리자 승인 → 목록에서 사라지고 팀원으로 편입(일반사용자 역할 유지) ⑦ 계정 비활성화 뒤 로그인 403, 계정 삭제 뒤 로그인 401 ⑧ 가입 안내 메일 실제 수신 확인.
- **코드 재검토 반영** — 저장소 740줄을 회사 부분 분리로 508줄까지 내림(700줄 규칙), 초대 메일 본문 HTML 이스케이프, 지도 타일 z/x/y 범위 검증, 회사명·사업자번호 중복 시 409, 값 변경 없는 회사 수정이 404 로 튕기던 문제 수정.
---
## 3. B05 — 종단설계
### 2-2. 3D 구조물 선택이 B06 횡단도에 안 뜬다 (2026-09-05 사용자 지시)
**선행:** 0-1 (넘김값을 ② 초안 한 자리로 흡수한 뒤 그 위에서 고침 — 지금 임시 키 위에서 고치면 두 번 일함).
넘김 자체는 서 있다 — 3D 클릭이 `aislo:structure-pick:{프로젝트}` 세션 한 칸에 `{측점, 부재키}`를 적고(`B05_Profile_UI_Page.ts`의 `wireStructurePick`), B06 진입 때 그 값으로 카드를 열게 돼 있다(`B06_Section_UI_Page_Structure_Pick.ts`). 실제로는 안 열린다. 의심 지점은 세 곳이다.
① **측점 대조 허용오차가 0.01m**로 좁다 — 3D가 준 누가거리가 소수점 아래에서 조금만 달라도 그 측점을 못 찾고 조용히 끝난다. 같은 파일의 다른 대조는 0.05m를 쓴다.
② 목록·폼에서 고르는 길(`B05_Profile_UI_Page.ts`의 `onPipeFacilitySelect`)이 같은 칸에 **측점만** 적어 부재키를 지운다 — 3D로 고른 직후 선택 동기화가 이 길을 타면 카드는 열려도 조정창이 안 열린다.
③ 복원이 **진입당 한 번**(`pickRestored` 빗장)인데, 부르는 곳이 둘(구조물 패널 적재 뒤 · 횡단 그림 그린 뒤)이라 이른 쪽이 빗장을 걸어 늦게 온 자료로는 다시 안 돈다.
**두 화면은 한 페이지처럼 움직여야 하므로 선택·하이라이트 연결선을 끊지 않게 조심할 것.**
**결론(2026-09-06 실측) — 코드 결함 없음. 넘김은 처음부터 서 있었음.**
안 되던 진짜 이유는 **클릭이 캔버스에 닿지 않은 것**임. 클릭 자리의 최상단 DOM 이
캔버스가 아니라 **종단 오버레이의 SVG `rect`** 였고, 이벤트가 `window` 캡처에만 뜨고
캔버스 리스너는 하나도 안 불렸음 — `pickAt` 이 아예 안 돌았음. 종단 오버레이를 접고
같은 좌표를 누르니 한 번에 통과했음.
- [x] 실화면 3D 클릭 — 세션 칸 `aislo:draft:structure-pick:{프로젝트}` 에
`{"at":85.18405,"key":"outlet"}` 이 적힘(기대한 꼴 그대로).
- [x] B06 진입 — 카드 64개 중 **그 측점 카드(4+5.2 = 85.2m 배수관) 1개가 선택된 채로** 뜸.
버려지는 단계 없음. 의심 지점 ①②③ 은 전부 해당 없음.
- [x] 코리도 그룹 레이캐스트도 정상 — 그룹 안 첫 **메쉬**가 `corridor-structure:revet:0`
(`at 85.18405` · `key outlet` · `visible:true`). 가리는 메쉬도 안 보이는 메쉬도 없었음.
「안 보이는 메쉬 건너뛰기」 수정은 **넣지 않음**.
⚠ **검증 함정 — 다음에 3D 클릭을 잴 때 반드시 지킬 것.**
`__corridorScene.project()` 가 주는 좌표는 **캔버스 좌표계라 DOM 이 덮고 있는지를 안 봄**.
클릭 직전에 `document.elementFromPoint(x, y) === document.querySelector('canvas')` 를
확인하거나, 종단 오버레이 핸들(`.ui-workflow-overlay__handle`, 텍스트 「종단」)을 먼저 접을 것.
이 함정을 몰라 「3D 픽 고장」으로 두 번 오진했음(지표면 가림 가설·`blocked()` 가설 둘 다 폐기).
### 2-3. 종단 그래프 팬 이동 — 좌클릭 끌기·휠 버튼 끌기
가로 이동이 이미 한 벌로 모여 있음(`B05_Profile_UI_Profile_Wheel.ts`). 여기에 끌기를 더하면 이것을 쓰는 **세 곳(종단 그래프·유토곡선·측점 테이블)이 한 번에** 같이 얻음.
- [x] 같은 파일에 포인터 끌기 이동 추가 — 진행 중이던 관성 이동은 끌기 시작 시 중단.
- [x] 그래프 위 조작 부품에서 시작한 끌기는 제외 — 계획고 편집 버튼(`.b05-profile-edit__btn`)·구조물 표식(`B05_Profile_UI_Structures_Marks.ts`)이 좌클릭을 이미 씀.
- [x] 끌기와 클릭 구분 — 몇 px 넘게 움직인 뒤에만 이동으로 보고, 그 경우 클릭(측점 선택·선택 해제)은 취소.
- [x] 끄는 동안 커서 모양 변경, 창 밖에서 손을 떼도 풀리게 함.
- [x] 실화면 확인(2026-09-06) — 측점 테이블에서 180px 끌자 세 곳 `scrollLeft` 가 0 → 180
으로 함께 움직임. 유토곡선에서 150px 더 끌자 셋 다 **330** 으로 맞물림.
측점 선택·계획고 편집은 그대로(같은 화면에서 계획고 ▲ 두 번 눌러 값이 바뀜).
### 2-4. 종단 테이블 행 정리 — 목적은 화면 자리 확보
지금 12행 = 구배 3행(L·H·S) + 측점값 7행 + 곡선 2행. 자리는 `B05_Profile_UI_Profile_Table.ts` 한 곳(`SEGMENT_ROWS`·`buildStationRows`).
- [x] 뺄 6행 — 구배 L, 구배 H, 절토고, 성토고, 누가거리, 거리.
- [x] 남길 6행 — 구배 S, 계획고, 지반고, **측점**(사용자 확정: 유지), 곡선 L, 곡선 R.
- [x] **B07 도면 종단도는 현행 12행 유지** — 도면 표는 별도 코드(`B07_DesignDetail_Engine_Cad_Table.py`)라 이 변경이 닿지 않음을 확인만 함(코드 확인 완료, 실제 도면 출력으로 재확인).
- [x] 행이 줄어든 만큼 그래프가 넓게 쓰이는지, 글자 크기·높이가 자동으로 맞는지 실화면 확인.
- [x] **높이 상한 신설**(2026-09-06 사용자: 「최대값 제한, 레이아웃 제어는 현재 유지」) —
6행 × 행 30px + 24px = **204px**. 리사이저 상한·패널 비례 연동·세션 복원 어느
경로로 들어와도 이 값을 넘지 않게 한 곳(`clampHeight`)에서 막음. 끌어 줄이는 것은
그대로(하한 140px).
- 자체검증 — 화면 실측 **167px**(상한 이하), 6행 모두 읽힘. 12행 시절 저장값(300px)이
남아 있어도 리사이저 복원이 상한으로 깎임.
### 2-5. 유토곡선 — 초기 화면 정리 + 최종 누가량을 종단 상단줄로
두 일이 같은 파일(`B05_Profile_UI_Profile_MassHaul.ts`)을 만지므로 한 항목으로 묶음. 순서는 (가) → (나).
**(가) 초기 화면은 곡선만 — 토량분배 숨김 (B05·B06 공용)**
기본 켜짐 목록은 `common_util/common_util_mass_haul.ts`의 `MASS_HAUL_DEFAULT_VISIBLE`(횡단 곡선)에 토량분배 키를 더한 형태이고, 그 더하는 자리가 B05(`B05_Profile_UI_Profile_MassHaul.ts`)·B06(`B06_Section_UI_Section_View_Panel.ts`) **두 곳**임. 세션 저장 키는 두 화면 공용.
- [x] 두 파일의 기본 목록에서 토량분배를 뺌.
- [x] 세션 저장 키 판(`b06:masshaul-visible-v4`)을 올림 — 안 올리면 이미 켜 둔 브라우저가 옛 값을 그대로 씀. (0-1 키 규칙이 먼저 서면 그 규칙의 새 키로 바로 감)
- [x] 범례에서 켜면 평형선·운반 블록이 예전대로 나오는지 확인 — 켠 뒤 분배 도형 80개 확인.
**(다) 토량분배를 켜면 곡선 전체를 한 화면에 (2026-09-06 사용자 지시)**
평형선·운반 블록은 노선 **전체를 놓고** 읽는 그림이라, 종단 그래프에 맞춘 가로 스크롤
안에서는 한 번에 못 봄. 분배를 켠 동안만 X·Y 비율을 버리고 컨테이너에 맞춤.
- [x] 분배가 켜져 있으면 곡선 폭 = 스크롤러 안쪽 폭(가로 스크롤 없음), Y 는 전 구간 기준.
- [x] 끄면 예전대로 종단과 맞물린 가로 스크롤·보이는 구간 Y 로 복귀.
- 자체검증 — 켠 상태 스크롤 내용폭 **867 = 보이는 폭 867**(스크롤 없음), 끈 상태
**4,600 vs 867**(예전대로). 분배 도형 80개, 종단 기준 곡선 선택 상태 유지.
- [x] 전체 맞춤일 때 **측점 세로선 숨김** — 67개가 촘촘히 서서 곡선을 덮기만 하고 자리도
못 읽음(2026-09-06 사용자 지시). 끄면 다시 나옴.
- [x] 전체 맞춤일 때 **좌우 여백 축소** — 종단과 맞물릴 필요가 없으므로 반 칸 들여쓰기
(originOffset)를 빼고 우측 여백을 12px 로 줄임. 곡선이 폭을 다 씀.
- 자체검증 — 측점선 **0개**(끄면 66개), 곡선 오른쪽 끝이 화면 끝까지 닿음.
- [x] **상단 여백 축소** — 요약 막대가 종단 상단줄로 올라간 뒤에도 범례가 옛 자리(34px)에
떠 있어 위가 비었음. 범례 4px, 곡선 상단 여유 40 → 30px(범례 높이 24 + 여유).
- 자체검증 — 범례가 오버레이 위에서 **5px**, 높이 24px. 곡선이 그만큼 위로 올라옴.
**(나) 최종 누가량을 종단 상단줄로**
지금 요약 막대(절토·성토·잉여/부족·최종 누가토량·운반·검산)는 유토곡선 오버레이 안에 있어 **패널을 접으면 사라짐**. 계획선을 만지며 봐야 하는 값이므로 종단 상단줄로 올림.
- [x] 종단 상단줄(`B05_Profile_UI_Profile_Balance.ts`)의 「최대 기울기」 **오른쪽**에 최종 누가토량 항목 추가 — 값 출처는 유토곡선 계산 결과(`result.points` 마지막 누계)이며 토량분배 켜짐과 무관하게 나옴((가)와 충돌 없음).
- [x] 유토곡선 오버레이의 **요약 막대 행 삭제**.
- [x] 삭제되는 나머지 값(절토·성토·잉여/부족·운반·검산)은 새 항목의 **툴팁**으로 내림 — 지금 요약 막대가 세부 내역을 툴팁에 두는 방식과 같음.
- [x] 종단 상단줄 높이를 조금 높임(값이 한 줄에 들어오게).
- [x] 유토곡선 펼침 손잡이를 **오버레이 형태**로 바꿈(그래프 자리를 먹지 않게).
- [x] 요약 막대·손잡이가 비운 만큼 유토곡선 그래프 상하 높이를 넓게 씀.
- [x] B06 화면의 요약 막대는 **그대로 둠**(같은 공용 함수를 쓰므로 B05 쪽만 떼어 내게 손댈 것).
- [x] 실화면 확인(2026-09-06) — 유토곡선 패널을 **접어도** 종단 상단줄에 「누가토량 547.8㎥」
가 그대로 남음. 계획고를 두 번 올리자 **547.8 → 528.1㎥**, 「편집 측점」 6 → 7 로 따라 바뀜.
### 2-5c. 유토곡선 자리 정리 — 총괄값 배지 + B06 곡선 제거 (2026-09-06 사용자 지시)
- [x] **기준은 횡단 고정** — 총괄값 계열을 범례 선택과 무관하게 횡단 기준으로 뽑음
(종단 기준 곡선은 뒤에 없앨 예정이라는 사용자 확정).
- [x] **최종 누가토량 배지** 신설(`common_util_mass_haul_badge.ts`) — 종단 그래프 좌측 상단에
겹쳐 뜸. 세부(절토·성토·잉여/부족·기준)는 툴팁.
- [x] B05 상단줄에서 「누가토량」 항목을 빼고 배지로 옮김. 유토곡선 패널은 **그대로 유지**.
- [x] B06 유토곡선 그래프·범례·요약줄 **삭제** — 그 높이를 종단면도가 다 씀. 같은 배지가
총괄값을 보여 줌.
- 자체검증 — B06: 배지 「누가토량 −5,595.5㎥」, 범례·요약줄 없음, 종단 그래프가 남은 높이
사용(228px), 횡단 카드가 그만큼 위로 올라옴. B05: 배지 「누가토량 6,842.6㎥」,
상단줄에는 누가토량 없음(최대 기울기·편집 측점만).
- 배지 자리는 Y축 폭만큼 밀어 축 라벨과 겹치지 않게 함(`--mass-haul-badge-left` 86px),
B05 는 상단 도구줄 아래로 한 줄 내림(`--mass-haul-badge-top` 34px).
- [x] **뒤이은 변화 (2026-09-06 저녁, 0-8)** — 이 절 뒤에 유토곡선 구조가 한 번 더 바뀌었으니
이 절만 읽고 판단하지 말 것:
- **배분·운반거리는 브라우저가 계산하지 않음.** 서버 `/haul-plan` 이 내고 화면은 편집이
멈추면 받아 둠(`B05_Profile_Api_HaulPlan`). 보안 목적임.
- 죽은 파일 `B06_Section_UI_Section_View_MassHaul.ts` **삭제됨** — B06 곡선 제거의 뒷정리.
- 접힘일 때는 **횡단 기준만** 적분함(`computeMassHaulSeries` 기준 인자).
### 2-6. 구조물 측점 누가거리 정본 일원화 — 남은 작업
저장 정밀도와 화면 정본 맞춤 완료분은 정리 전 원문 스냅샷에 보존했다.
- [x] 겹치면 구조물 측점만 남긴다.
- [x] 그래프 측점 목록에서 구조물 측점과 0.1m 안에 겹치는 규칙 측점을 제외한다 — 공용 유틸 `dropStationsNear`(`B05_Profile_Util_Station.ts`), 종단 그래프와 3D 측점 띠가 같은 규칙을 씀.
- [x] 구조물 측점 라벨 실화면 확인(2026-09-06) — 종단 그래프에 「찰쌓기 1~4」·「옹벽」·
「배수관 1」 이 이름표와 함께 섬.
- [x] 선택 연동 확인 — 그래프의 「옹벽」 마크를 누르자 마크가 선택되고 사이드 목록의
「5+15.0~6+5.0 옹벽」 도 함께 선택되며 폼에 기준측점 5 가 실림. 측점 테이블 **열
강조는 없음**(구조물 구간은 정규 측점 열이 아님 — 표시 규칙은 사용자 판단).
- [x] B06 종단 구조물 표시를 B05 와 통일 (2026-09-07 사용자 동의, 보조 창 커밋 `67972612`) —
B05 가 쓰는 **같은 부품**(`buildStructureLane`)을 B06 종단 아래에 붙임. 실화면에서
알약 10개(배수관 1~8 · 세월교 1~2)가 B05 와 같은 이름·번호로 뜸. 관을 가상 구조물로
바꾸는 함수는 B05 안에 갇혀 있던 것을 공용으로 올려 **한 벌만** 둠. 알약을 누르면 그
측점 카드가 열림. 끌기는 B05 몫으로 남김(배수유역 재분할이 걸림).
- [x] 유토곡선이 구조물 **초안까지** 반영하는지 확인(2026-09-06) — 같은 화면에서 두 곡선을
계산해 대조: 초안 벽 포함 **−9,286.7㎥**, 서버 저장분만 **10,288.7㎥**(차이 1,002㎥).
앞서 「값이 안 변한다」고 본 553.4㎥ 는 **종단 기준**(개략) 곡선이라 횡단 면적과
무관했던 것 — 횡단 기준으로 바꾸면 초안이 반영됨.
### 2-7. 초기값(초기 스냅샷) 후속 — 실경로 검증
배수유역 스냅샷 범위 확대·옛 스냅샷 호환·3D 코리도 보존 구현과 단위검증은 정리 전 원문 스냅샷에 보존했다. 노선 변경 게이트 설계는 0-2 로 옮김.
- [x] 확인(2026-09-06 용화 신규 프로젝트) — 파일 업로드부터 체인 완주까지 화면에서 돌렸고 `initial_snapshot/`(노선·종횡단·배수유역·코리도·DB덤프)이 생성됨.
- [x] **`initial_design.lock` 흐름 확인**(2026-09-06 보조 창, 공용 브라우저) — 락을 세우면
`surface/status` 가 `in_progress`·`initial_design` 이 되고 단계바 [종단설계] 클릭 시
`#/b11-loading` 으로 튕김(문구·0% 표시). 락을 지우면 **약 2초 만에 `#/b05-profile` 로
자동 복귀**(3초 폴링과 맞음).
⚠ 단계바 `.ui-workflow-layout__step` 은 **0번이 대시보드**라 종단설계는 3번임.
---
## 4. B06 — 횡단설계
3-2 가 「구조물이 서면 면적이 달라진다」는 자리를 만들고, 3-4·3-5 가 그 위에 구조물 형상을 얹는 순서임.
### 3-1. 곡선부 노폭 자동 확폭 (평면 R 기준) — 새 기능
**선행:** 0-3 (측점별 평면 R 계산). 0-2 계획노선이 바뀌면 R·확폭도 같이 다시 섬.
노폭 = **도로 중심의 차도 폭**(노견 아님). 지금 코드에 곡선부 확폭이 **없음** — 차도 폭은 규격 프리셋 상수 하나로 전 측점 동일(`B06_Section_Engine_Design.py`의 `road_width_m`). 기존 「노폭 연장」은 기슭막이가 밖으로 나간 만큼 노견이 따라 늘어나는 별개 계산임.
**근거 (2026-09-06 지식DB·실무 원본 확인)**
- 법령(별표2 .2.나.(4) = `평면선형.md` §3) — 확폭량은 **평면 곡선반경 R** 로만 정해지며 표값은 하한:
| R(m) | 10~13 | 13~14 | 14~15 | 15~18 | 18~20 | 20~25 | 25~30 | 30~40 | 40~45 | 45 이상 |
|---|---|---|---|---|---|---|---|---|---|---|
| 확폭(m) | 2.25 | 2.00 | 1.75 | 1.50 | 1.25 | 1.00 | 0.75 | 0.50 | 0.25 | 없음 |
- 유효너비 3.5m 이상이고 확폭 구간은 **최대 5m까지**. 배향곡선지는 8m 이상.
- 실무 원본 대조 — 울진·대흥 2공구 수량서 「확폭」 시트: IP별 R → 표값 W 그대로, 면적 = ((L1+L2)/2)×W 사다리꼴이고 **전이는 양끝 각 10m**. 울진·소광 「포장확폭」 시트: **측점별 확폭폭 × 구간거리** 로 면적 산출. 대피소는 두 곳 모두 **별도 줄로 측점 범위·면적 직접 입력**.
**확정 사항 (2026-09-06 사용자)**
- **확폭 방향 = 곡선 바깥쪽 편측** — 회전 시 차량이 밀리는 쪽 기준. (참고: 일반 도로설계기준은 후륜 내륜차로 **안쪽** 확폭이며, 임도 법령·교본에는 방향 규정이 없음. 도면 실물로 재확인할 여지 있음)
- **전이 = 양끝 각 10m 테이퍼**, 확폭 유지 구간은 곡선 구간(BC~EC). 측점별 확폭폭을 갖는 방식으로 넣으면 실무 사다리꼴과 같은 결과가 됨.
- [x] 위 표로 확폭량 W 를 정함(경계는 「이상~미만」, 45m 이상 없음, 표값은 하한) — R 은 0-3 이 실어 준 값을 씀.
- [x] 곡선 구간 앞뒤 **10m 테이퍼**로 확폭폭을 0→W→0 으로 잇고, 측점별 값(`curve_widening_m`)으로 저장 — 설계는 반경 표값이 아니라 이 값을 우선 씀(파이썬·TS 짝 모두). 측점 간격 20m 에서는 중간 측점이 없어 화면 보간이 대신 이어 줌.
- [x] 차도 폭 = 규격 폭 + 확폭(**바깥쪽 편측**)으로 측점별 적용 — 노견·측구·사면은 그 바깥에 그대로 밀림.
- [x] 확폭 뒤 유효너비가 **5m를 넘지 않게** 제한하고, 넘으면 경고 표시.
- [x] 횡단도에 **노폭 값 라벨** 표시(확폭 걸린 측점을 눈으로 확인 가능하게).
- [x] 3D 코리도는 설계선(`road_edges`·`carriageway_edges`)을 그대로 읽으므로 확폭이 자동으로 실림. 수량 쪽은 B08 재작업 대기 — 그때 `carriageway_width_m`(= 3.0 + 확폭)를 쓰면 됨.
- [x] 대피소·차돌림곳은 자동계산에 들어가지 않음 — 확폭은 평면 곡선반경만 보므로 구조적으로 제외됨. 별도 구간 입력은 뒤에 정함.
- [x] 파이썬·브라우저 두 벌 계산이 한 세트이므로 같이 고치고 거울 테스트를 돌림.
- [x] 실화면 확인(2026-09-06 용화 신규 프로젝트) — 31곳 확폭, 67곳 중 64곳이 표값과 정확히 일치, 나머지 3곳은 테이퍼로 생긴 값(0.40·0.07·0.19m). 유효너비 5m 상한도 걸림(R12.2 → 표값 2.25 대신 2.00). 카드 라벨 「노폭 5.00m (규격 3.00 + 확폭 2.00)」.
### 3-2. 구조물 절·성토 면적 반영 (2026-09-05 사용자 착수 지시)
**연결:** 3-4·3-5 가 구조물 형상을 바꾸므로, 그 결과가 이 면적 계산으로 들어와야 함 — 면적 반영 자리를 먼저 만들고 둘을 그 위에 얹는 순서가 맞음.
보류를 푼다. 지금 면적은 **구조물이 없다고 보고 낸 값**이고, 그 뒤 구조물이 들어오면 면적이 갱신돼야 한다. 상세 원인·실측값은 정리 전 원문 스냅샷에 보존했다.
**코드 확인 결과 (2026-09-05)** — 면적은 지반선과 설계선의 **차이만 사다리꼴로 적분**한다(`B06_Section_Engine_Areas.py` ↔ `common_util/common_util_cross_design_areas.ts`, 두 벌 한 세트). 이 계산은 구조물이 있는지조차 모른다. 구조물 형상을 깎는 일은 **3D 쪽에서만** 하고 있다(`B05_Profile_UI_Corridor_Carve.ts` 등) — 그 결과가 면적으로 돌아오지 않는다. 그래서 「구조물이 먹은 만큼 면적을 고치는」 자리를 새로 만들어야 한다.
설계선을 세우는 `B06_Section_Engine_Design.py`가 이미 671줄이라 700줄 여유가 없다 — **새 로직은 별도 파일**로 뺄 것.
- [x] **기준 확정(2026-09-06 사용자)** — 구조물 자체 면적을 빼는 것이 아니라, 지반선과 **실제로 그려지는 설계선**이 이루는 폐회로의 넓이다. 그리는 쪽 트림(`designTrim`)을 그대로 받아 계산하므로 시설 종류(기슭막이·BOX·세월교)를 가리지 않는다.
- [x] 브라우저 계산에 반영 — `common_util_cross_structure_areas.ts` 신설, 벽면을 수직으로 끊어 단차에서 없는 면적이 생기지 않게 함. 확인: 전구간 100㎡ · 벽에서 끊음 6.25㎡ · 구조물선 포함 20㎡.
- [x] **서버에도 같은 값이 서게 함** — 파이썬 포팅이 아니라 0-4 의 Node 진입점으로 해결. 전처리 체인·[저장]·[확정]에서 서버가 같은 TS 를 돌려 정본에 얹는다. 카드를 안 그린 측점도 값이 선다.
- [x] 확인(2026-09-06) — 카드 버튼을 바꾸면 브라우저가 전 측점을 다시 계산하고, [저장] 때 면적·유토곡선이 함께 정본에 실림(DB 대조 완료).
- [x] 파이썬 짝은 만들지 않기로 함 — 기하는 화면 그리는 코드와 한 벌이어야 해서 Node 실행을 택함(0-4). 거울 테스트 대상이 아님.
- [x] 남은 확인 — 구조물을 **새로 추가**했을 때 면적표·유토곡선이 같이 움직이는지 (2026-09-06 실화면).
- 결과: **안 움직임.** 좌측 「구조물 배치」로 C군(돌쌓기(찰)·옹벽)을 넣고 [저장]까지 해도
그 측점 절토계(4+0.0 = 5.05㎡)와 최종 누가토량(553.4㎥)이 그대로임.
- 까닭: 면적을 만드는 구조물 폐회로는 **측점 카드의 횡단 설계값**(`revet_adjust
`extra_wall_counts`·`inlet_structure` 등)에서 나오고, `structures.json` 목록은
그 계통에 실리지 않음. 즉 두 「구조물」이 서로 다른 자리에 있음.
- [x] **사용자 확정(2026-09-06): 반영한다.** 좌측 목록의 C군 벽(옹벽·돌쌓기·흙막이·큰돌쌓기)을
횡단 제원으로 얹어 도면·면적·유토곡선이 함께 보게 함.
- 방법 — 독립 기슭막이가 쓰던 `section.revetment` 제원 자리에 그대로 얹음. 그러면 기하·
설계선 트림·폐회로 면적·3D 가 손대지 않고 따라옴(옛 `attach_revetments` 를 C군으로 되살림).
- 서버 `B06_Section_Engine_Structures_Wall.py` ↔ 브라우저 `common_util_structure_walls.ts`
**짝**(거울 테스트 `tmp/tests/test_b06_structure_walls_mirror.py`). 서버는 저장분을,
브라우저는 **아직 저장 안 한 초안**을 얹음 — 조작 중에도 즉시 보이게.
- 설치 측은 C군 폼에 칸이 없어 **성토가 나는 쪽**으로 자동(양측 절토면 벽을 세우지 않음).
- 형태는 종류로 이음(돌쌓기(찰)/(메) 그대로, 옹벽→콘크리트, 흙막이→통나무·목재틀,
큰돌쌓기→돌쌓기(메)) — 형태가 높이 한계·두께를 정하기 때문.
- 자체검증 — 40m 에 돌쌓기(찰)을 넣자 그 측점 **성토 5.81 → 5.02㎡**(0.79). 절토 측점
(120m, 우측 절토)에서는 값이 그대로 — 성토 사면이 없어 벽이 면적을 바꾸지 않음(정상).
초안 3건을 브라우저가 얹어 면적 행 15 → 18개. 거울 테스트 통과.
- [x] 화면 배선 확인(2026-09-06 실화면) — 구조물을 넣는 **즉시** 그 측점 카드와 유토곡선이
함께 움직임. 5+0.0 성토 10.43 → 7.88㎡, 3+0.0 성토 8.06 → 5.73㎡ ·
최종 누가토량 553.4 → 546.4㎥. 저장은 누르지 않았음(조작 중 계산은 브라우저 몫).
- 고친 것 — 유토곡선은 카드보다 **먼저** 계산돼 처음에는 카드만 바뀌고 곡선이 옛 면적으로
남았음. 구조물이 바뀌면 면적을 먼저 다시 얹고 그리게 순서를 바로잡음.
- [x] 설치 측을 사용자가 고르게 함 (2026-09-07 사용자 확정 「폼 선택은 가능하게 반영하고
나중에 선택 비활성화로 하자」, 보조 창 커밋 `6b76a3f2`) — C군 5종(옹벽·돌쌓기 찰/메·
흙막이·큰돌쌓기)에 「설치 측: 자동(성토 쪽)·좌·우」 칸을 넣음. **기하는 이미 그 값을
보고 있었고**(`Cross_Revetment.ts:168`) 칸만 없던 것. 함께 옵션에 `enabled` 를 둬
**회색으로 잠그는 구조**를 만듦 — 잠기면 필수 검사에서도 빠짐.
⚠ 그 기본값 「자동(성토 쪽)」이 「근거 없는 기본값 금지」 시험에 걸려, **지금까지의
동작을 잇는 값**이라는 근거를 적어 허용 목록에 넣었음.
- [x] **구조물 저장이 통째로 유실되던 버그 수정** (2026-09-06 실화면에서 드러남) —
B05 화면이 진입 때 받은 판번호로 저장해, 그 사이 정본이 한 번이라도 바뀌면
409 로 거절되고 실패 처리가 **세션 초안까지 지워** 넣은 구조물이 사라졌음.
판번호를 쓰기 직전에 다시 받고, 실패해도 초안을 남기도록 고침.
- 자체검증 — 고치기 전: 구조물 추가 → [저장] → 정본 0건(사라짐). 고친 뒤: 추가 →
[저장] → 정본 **1건 → 2건**(판번호 3 → 4 → 5), 세션 초안은 저장 성공 뒤에만 비워짐.
### 3-4. 기슭막이 연동 해제 시 높이도 분리
**연결:** 3-2 위에 얹음(높이가 갈리면 면적도 갈림).
지금 연동 해제는 **위치(4축 조작값)만** 그 측점 것으로 갈림. 높이는 두 곳에서 오는데 ⓐ 구조물 옵션에 저장된 높이(연동 구간 공용) ⓑ 측점별 조정값 — 해제해도 그 측점에서 높이를 직접 만지기 전에는 ⓐ를 계속 따라감(`B06_Section_UI_Cross_Culvert_Wire.ts`의 `detachedAdjusts`, `B06_Section_UI_Cross_Culvert_Const.ts`의 `revetWallSpec`).
- [x] 연동을 푸는 **그 순간의 실제 높이를 그 측점 값으로 굳힘** — 값이 튀지 않으면서 이후에는 따로 놀게 함.
- [x] 다단(추가 기슭막이) 높이도 같은 규칙으로 분리.
- [x] 단 수·형태 유지 확인(2026-09-06 수치) — 연동 측점 260m 은 소유 측점 264.06m 과 같은
**벽 1개·다단 2개·형태 돌쌓기(메)·높이 1.30m**. `revet_link_detached` 를 켜도 그대로 —
해제하면 다단이 통째로 사라지던 옛 문제는 재발하지 않음.
- [x] **실화면 확인 완료 — 굳히기 정상**(2026-09-06 공용 브라우저, 용화)
- 벽이 있고 연동이 켜진 카드 **820.00m** 에서 [연동]을 끄자 세션 `revetx` 에
`820.00:outlet {h: 1.8}` 이 적힘 — **푸는 순간의 높이가 그 측점 값으로 굳음**.
- 그 뒤 소유 측점 **821.62m** 높이를 4.2m 로 올려도 820.00m 은 **1.8m 그대로** — 따로 놂.
3-4 의 본 취지가 성립함.
- ⚠ **측정 방법 두 가지를 틀리면 「안 굳는다」로 보임**(둘 다 실제로 겪음):
① 조정창 `.b06-structure-panel` 은 **카드마다 하나씩 67개** — 첫 번째만 보면 영영
`is-hidden`. **그 카드 안의 패널**(`card.querySelector`)을 볼 것.
② **벽을 안 그리는 카드**를 고르면 `drawnWallKeys()` 가 비어 굳히기 루프가 0회 돎.
`card.querySelector('.b06-chart__culvert-revet-hit')` 로 먼저 거를 것.
- 조정창 여는 법 — 판정면을 `scroll_into_view_if_needed()` 로 화면에 올린 뒤 그냥 클릭.
카드를 미리 고를 필요 없음.
- [x] **벽을 안 그리는 카드에 [연동] 버튼이 뜨던 것**(커밋 `71694163`) — 눌러도 아무 일이
없고 `revetlink` 에 `detached` 만 쌓였음. `linkState` 가 `drawnWallKeys().length` 도
함께 보게 함.
### 3-5. 연계(추가) 기슭막이가 성토사면과 만나는 자리 안 맞음 · 노견 확장 없음 (버그)
**연결:** 3-2 위에 얹음(노견이 늘면 면적이 바뀜).
> **2026-09-06 상태** — 코드만 보고는 고칠 자리를 못 좁힘. 추가 벽은 노견이 아니라 관 하단 꼭짓점에서 1:1.2 로 내려오는 성토부선을 타므로, 노폭 연장이 안 걸리는 것이 잘못인지 아닌지가 화면 실측 없이는 안 갈림. **화면 재현 뒤 수리**로 미룸.
확인 결과 — 노견을 밖으로 늘리는 계산(`B06_Section_UI_Cross_Culvert_Solve.ts`의 `fillSlopeOf`)은 **배관 기준벽(유입·유출)에만** 걸림. 추가 기슭막이(다단)는 `B06_Section_UI_Cross_Culvert_Extra.ts`가 따로 세우며 그 길을 타지 않음.
- [x] 증상을 수치로 잼(2026-09-06) — **추가 벽 바닥이 원지반에서 떠 있었음**.
배수관 측점에 다단 1을 세워 재니 `floatGapM` = 264.06m **0.394m** · 731.31m 0.873m ·
821.62m 0.427m · 914.93m 0.122m. 성토부선과 벽 **상단**은 원래 정확히 만남
(성토선이 벽 `topJoint` 로 끝나게 만들어짐) — 어긋난 것은 **아래쪽**이었음.
- [x] 원인 가름 — 노견 확장(`fillSlope.roadWideningM`)은 이 측점들에서 기준벽도 0 이라
원인이 아님. 추가 벽은 상단을 1:1.2 성토선에 붙이고 **키는 요청값 그대로** 쓰는데,
지형이 그만큼 안 나오면 바닥이 뜬 채로 남았음(코드 주석도 「뜨면 경고」로 두고 있었음).
- [x] 고침 — 자리는 그대로 두고 **키를 지반까지 맞춤**: 지반이 낮으면 키우고(형태 높이
한계까지), 높으면 줄임(최소 높이까지). 되받기 값도 조정된 높이로 나감.
- 자체검증 — 같은 측점 재측정: 264.06 **0.394 → 0.000m**(키 1.00 → 1.39),
821.62 0.427 → 0.000, 914.93 0.122 → 0.000, 1002.24 0.000. 731.31 은 0.873 → 0.373 으로
줄되 남음 — 돌쌓기(메) 높이 한계(2.0m)에 걸린 자리라 예전처럼 경고를 띄우는 것이 맞음.
- 테스트 390 통과.
- [x] **3D·횡단도·면적이 한 벌인지 코드로 확인**(2026-09-06) — 셋 다 `computeCulvertLayout`
한 함수를 지남: 3D `B05_Profile_UI_Corridor_Structures.ts:25`, 횡단도
`B06_Section_UI_Cross_Culvert_Wire.ts:8`, 면적 `structureAreaRows` →
`computeStoredLayouts`. 구조적으로 갈릴 수 없음.
- [x] **ⓐ 다단 벽이 원지반에 앉는지 — 통과**(2026-09-06 보조 창, Node 로 저장 경로 실행)
- **270.43m** — 단 **2개 실제로 섬**(`extra_wall_counts {outlet:2}` 그대로 반영).
벽1 바닥 826.291 · 높이 0.967 · **뜸 0**, 벽2 바닥 825.265 · 높이 0.512 · **뜸 0**.
끝 성토 0.912/0.861/0.117m 로 `addable=false`(더 못 놓음).
- **85.18m** — 단 0개인데 **정상**. 끝구간 갈래가 `cut` = 시작점이 원지반 아래(매몰)라
관문 ①에 걸림. 그 측점은 다단 대상이 아님.
- ⚠ 앞서 「단 수는 저장됐는데 벽이 0개」로 본 보고는 **오측**(조정창을 안 거치고 API 로만
심은 뒤 다른 자리를 봤음). 배선·기하 모두 정상임.
- [x] 🔴 **다단이 면적(물량)을 안 바꾸던 것 — 고침** (2026-09-07 사용자 지시 「옹벽도 수량이
변되어야함」, 보조 창 커밋 `e9ad8cdf`)
- 고친 것 — 면적 폐회로의 바깥 경계가 **기준벽 사면까지만**이라 그 바깥은 원지반으로 보고
0 이었음. 화면이 그리는 선(`outletFill.segments`·`basinFill.segments`)을 트림 사면에
**이어 붙임**. 「면적은 실제로 그려지는 설계선을 따른다」는 기존 원칙과 같은 자리로 맞춘 것.
그리지 않는 `cut` 갈래(벽이 원지반에 묻힌 자리)는 화면과 같게 뺐음.
- 전후 표(용화 route 165) — 81.77m 성토 **8.4247 → 10.9027**(단 0) · 단 1개면 10.6743
(**단이 서면 성토가 줄어듦** — 벽이 흙을 대신 받으니 맞는 방향) · 244.14m 3.3492 → 4.0511 ·
423.55m **변화 0**(벽이 묻힌 자리). 절토는 전부 불변.
- 81.77m 은 지형이 **1단까지만** 허락해 2·3 요청이 같은 값임(관문 두 개) — 결함 아님.
- 파이썬 짝 없음 — 구조물 트림 면적은 TS 한 벌이고 서버가 Node 로 돌림.
- [x] 🔴 **그 파급 — 사용자 확정: 그대로 둠**(2026-09-07 「1번 ok」). 되돌리지 않음.
아래는 판단 근거로 남김.
- 같은 수정으로 **단이 하나도 안 서는 측점의 성토도 늘어남**(81.77m +2.48㎡ · 244.14m +0.70㎡ ·
벽이 묻힌 423.55m 은 0). 이유는 같음 — 기준벽 **바깥으로 이어지는 성토부선**이 화면에는
그려지는데 면적에는 안 들어가 있었음.
- 관점 ⓐ **정정임** — 벽 바깥 흙쌓기는 실제 물량인데 빠져 있었음(과소 산정).
관점 ⓑ **범위가 넓음** — 배관·세월교가 선 **모든 측점**의 성토가 늘어 견적 금액이 오름.
사용자 지시는 「옹벽도 수량이 변경되어야 함」이었고 이 변화는 옹벽 단 수와 무관함.
- 두 창 모두 ⓐ 가 맞다고 보나 **금액에 바로 닿아 사용자 확인이 필요**함.
- 되돌리려면 「단이 2개 이상일 때만 이어 붙이기」로 좁힐 수 있으나 그러면 화면과 면적이
다시 어긋남. **되돌릴 한 줄 자리는 `Cross_Culvert_Geom.ts` 의 `extendTrimSlope` 위 주석**에
박아 뒀음.
- 아래는 고치기 전의 조사 기록 —
- 실측(보조 창) — 같은 자료로 `extra_wall_counts` 만 2 → 0 으로 바꿔 `structureAreaRows` 를
두 번 돌리니 **차이 0.0000**. 270.43m 절토 1.0246/성토 7.5471, 85.18m 절토 5.1737/성토 0.7124.
- 원인 — 면적은 `designTrim` 으로 잘린 폐회로에서 나오는데, 그 트림 경계는
`Cross_Culvert_Geom.ts:632` 에서 **기준벽(유입·유출)의 성토 사면**(`fillSlopeOf`)으로만
잡힘. **다단의 성토 구간(`extras.segments`)은 트림에 안 실림.** 그래서 단이 몇 개든
폐회로가 같음.
- **판단이 필요한 이유** — 다단이 서면 성토면이 계단으로 바뀌므로 실제 토량은 달라짐.
3-2 의 원칙(「구조물이 서면 폐회로가 달라진다」)대로면 **면적도 바뀌어야 함**.
다만 이 값은 **B08 수량·B09 견적으로 그대로 나가는 물량**이라, 임의로 바꾸면 견적이
조용히 달라짐. 사용자 확인 뒤 고칠 것.
- 고칠 자리는 좁음 — 트림을 기준벽 사면 대신 **다단이 있으면 그 구간을 이어 붙인 선**으로
잡으면 됨(`trimMaxSlope.points` 를 `extras.segments` 로 이어 만들기). 3D·그리기·수량이
같은 트림을 쓰므로 셋이 함께 움직임.
---
## 5. B07 — 상세설계(도면)
### 4-1. 도각 외부 파일 불러오기 — DWG 받기 + 자리표 배치 (2026-09-06 사용자 지시로 방식 변경)
지금은 도각을 캐드 화면에서 직접 고쳐 회사 도각으로 저장하는 길만 있음(`B07_DesignDetail_UI_FrameEdit.ts` → 회사 도각 `storage/{회사}/templates/`, 프로그램 기본 도각은 `resources/template_2dDrawing/00_template_A1.json`). 여기에 **외부 파일 불러오기**를 더함. 고객이 내는 도각은 **DWG 일 확률이 높으므로 DWG 를 받아 변환**해 들이고, 프로그램 값이 들어갈 자리는 **자동으로 알아맞히지 않고 사용자가 아이콘·라벨을 놓아 지정**함(2026-09-06 사용자 제안 — 자동 연계 규칙을 만들 필요가 없어 훨씬 단순함).
**검토 결과 (2026-09-06) — 제안이 지금 구조와 정확히 맞음**
- **자리표 방식이 이미 서 있음.** 기본 도각의 글자 24개 중 12개가 `{{도면명}}`·`{{공사명}}`·`{{과업책임자}}`·`{{설계일자}}`·`{{용역회사}}`·`{{시행청}}`·`{{도면번호}}`·`{{분야별책임자}}`·`{{설계자}}`·`{{축척_A1}}`·`{{축척_A3}}` 같은 **자리표 토큰**이고, 그림 4개는 `{{회사로고}}`·서명 자리임. 출력 때 `B07_DesignDetail_Engine_Template.py` 의 치환 한 곳이 값을 채움(`_fill_placeholders`). **사용자가 자리표를 놓기만 하면 연계는 이미 끝난 상태** — 새 연계 로직 불필요.
- **DXF 읽기 라이브러리가 이미 깔려 있음** — `ezdxf 1.4.4`. 기본 도각 JSON 자체가 `source: 00_templete_A1.dxf` 로 남아 있어 **DXF → 도각 JSON 변환을 이미 한 번 거친 결과물**임. 같은 변환을 코드로 세우면 됨.
- **DWG 는 그대로 못 읽음** — 비공개 형식이라 `ezdxf` 도 DXF 만 읽음. 무료 외부 변환기(ODA File Converter)를 서버에 설치해 DWG→DXF 로 바꾼 뒤 읽는 것이 유일한 길. 변환기가 없으면 「DXF 로 저장해 올려 주세요」 안내로 떨어짐(폴백).
- 도각 JSON 의 요소는 `Text · Line · PolyLine · Point · Image` 5종. DXF 에서 이 5종만 추리면 됨.
**할 일**
- [x] 서버에 ODA File Converter 를 두고 **DWG → DXF 변환**을 붙임 — 없으면 DXF 만 받고 안내 문구를 띄우는 폴백.
- [x] **DXF → 도각 JSON 변환** — `Text · Line · PolyLine · Point` 를 추리고 좌표·도면층을 그대로 옮김. 지원 밖 요소(치수·해치·블록)는 어떻게 처리할지 정함(선으로 풀지, 버릴지).
- [x] 불러온 도각을 **회사 도각 저장 경로**(`storage/{회사}/templates/`)에 얹는 흐름을 잇고, 저장 즉시 다음 도면부터 쓰이게 함(캐시가 수정 시각을 키로 쓰므로 자동).
- [x] **자리표 배치 도구** — 넣을 수 있는 값 목록(위 12종 + 로고·서명)을 아이콘·라벨로 보이고, 도면 위 원하는 자리에 끌어다 놓게 함. 놓은 뒤 **위치·크기·정렬·글자크기**를 조절.
- [x] 놓인 자리표는 화면에서 **무슨 값인지 이름표로** 보이고, 저장할 때 `{{키}}` 토큰으로 굳음 — 출력은 기존 치환 엔진이 그대로 처리.
- [x] 요소 선택 시 **회색 외곽선** 표시.
- [x] 자리표를 안 놓은 값은 **빈칸**으로 남김(지금 규칙 그대로 — 남의 값이 남지 않음).
- [x] 실화면 확인 — DWG 불러오기 → 자리표 배치 → 저장 → 다음 도면 출력에 값이 채워지는지.
#### 자체검증 (2026-09-06, 보조 워크트리 8001·5174 공용 브라우저)
- DXF 변환 — 견본 도각(테두리·표제란·글자·원·블록·해치)을 만들어 불러옴. 선·폴리선·글자·점은 그대로 들어오고, 원은 선분으로 펴지고, 블록은 낱개로 풀리고, 해치는 빠짐(의도대로).
- DWG — **LibreDWG(`dwg2dxf`) 로 확정** (2026-09-06 약관 확인 뒤 변경). ODA File Converter 는 비회원 무료 사용이 **비상업 용도 한정**이라 상용인 이 프로그램에는 못 씀(상업 사용은 ODA 회원 가입 = 연 수천만 원대). LibreDWG 는 GPL 이지만 **별도 실행 파일로만 호출**하므로 이 프로그램 코드에 의무가 번지지 않음. 읽는 범위는 R2018 까지이며, 그 이후 형식·변환 실패·변환기 미설치는 「2018 DWG 또는 DXF 로 저장해 달라」 안내로 떨어짐. 변환기 경로는 `.env` 의 `LIBREDWG_DWG2DXF_PATH` (없으면 PATH 에서 찾음). 판정·안내 경로는 단위 테스트 4건으로 확인(`tmp/tests/test_b07_frame_import_dwg.py`) — 실제 DWG 변환은 변환기 설치 후 확인 필요.
- 자리표 패널 — **캐드 안 오른쪽 아래**에 뜸(2026-09-06 사용자 지시로 부모 사이드바에서 옮김). 도각 편집으로 도면을 실었을 때만 보이고, 단추 18개(글자 14 · 그림 4) 확인.
- 배치 → 저장 — 「공사명」·「회사로고」를 놓고 [완료]. 회사 도각 파일에 `{{공사명}}` 글자와 `{{회사로고}}` 그림이 그대로 저장됨(엔티티 10개).
- 도면 반영 — 종단면도를 다시 받아 보니 도각 글자가 `검증용 외부도각 · 회사명 자리 · 카드미리보기_검증_sub` 로, **자리표가 프로젝트명으로 채워짐**. 남은 `{{토큰}}` 없음, 로고 그림 자리도 값으로 치환됨.
- 그림 자리표는 값이 없으면 화면에 아무것도 안 보이던 문제를 고침 — 자리표일 때는 테두리와 이름을 늘 그림(출력물에는 안 실림).
- 글자를 집으면 회색 점선 외곽선이 생기도록 고침.
- 검증에 쓴 회사 도각은 **원본으로 되돌려 놓음**(검증 전 백업 → 복구 확인, 엔티티 51개).
- 테스트 — `pytest tmp/tests/ -q` 186 passed, 1 skipped.
### 4-2. 도면 내보내기 (DXF·DWG) + LibreDWG 동봉 (2026-09-06 사용자 지시)
불러오기의 반대 방향. 캐드 「출력」 리본에 **DXF 내보내기 · DWG 내보내기** 를 더함. 파일은 서버가 만든다 — 캐드가 도면을 부모에 넘기면 부모가 서버에 맡기고 내려받는다.
- [x] 내보내기 엔진 — 도면 JSON 을 ezdxf 로 DXF 로 씀(선·폴리선·글자·점·원·호, 도면층 이름 유지). 그림(Image)은 DXF 가 외부 파일을 가리키는 방식이라 제외하고 **몇 개를 뺐는지 알림**.
- [x] DWG 내보내기 — LibreDWG `dxf2dwg` 를 별도 프로세스로 호출(`.env` 의 `LIBREDWG_DXF2DWG_PATH`). 변환기가 없으면 「DXF 로 내려받으라」 안내.
- [x] 한글 파일 이름 — 헤더에 그대로 실으면 서버가 500 으로 떨어지던 것을 고침(영문 이름 + UTF-8 이름 함께 전달).
- [x] 도각·내보내기 엔드포인트를 `B07_DesignDetail_Router_Frame.py` 로 분리(700줄 제한).
- [x] **LibreDWG 동봉 완료** (2026-09-06 사용자 확정 — 저장소에 같이 둠). 실제 파일은 **메인 워크트리** `B07_DesignDetail/openwebcad/tools/libredwg/` 에 두고 보조 워크트리는 같은 자리에 **링크(junction)** 로 잡음 — 33MB 를 한 벌만 둠. 담은 것은 `dwg2dxf.exe`·`dxf2dwg.exe`·`libredwg-0.dll`·부속 dll 3개 + GPL 전문(`COPYING`) + 출처·교체법 README. **파이썬 바인딩은 제외**(import 하면 GPL 이 번짐). 변환기 찾는 순서는 `.env` → 동봉본 → PATH.
- [x] DWG 왕복 시 한글이 깨지던 것 고침 — 옛 DXF 는 한글을 유니코드 escape 로 적으므로 불러올 때 되돌리고, DWG 로 갈 DXF 는 R2004 로 만듦.
#### 자체검증 (2026-09-06)
- 종단면도(1)을 캐드 「출력 → DXF 내보내기」로 내려받음 — **335KB, 도형 1,833개**(선 1,490 · 글자 305 · 원 29 · 점 8 · 호 1), 도면층 `b08-ground·b08-design·b08-long-grid·b08-long-table·b08-frame` 그대로. 표제란 값은 채워진 상태이고 `{{토큰}}` 남지 않음.
- 화면 안내 — 「DXF 로 내보냈습니다. 그림 3개는 담기지 않았습니다.」(로고·서명 3개)
- DWG 내보내기 — 변환기가 없어 400 과 함께 「DXF 로 내려받아 주십시오」 안내 확인.
- **DWG 실검증 (동봉 뒤)** — 캐드 「출력 → DWG 내보내기」로 종단도(1)을 받음(**106KB**, 형식 2000). 그 파일을 다시 불러오니 선 1,490 · 글자 302 · 폴리선 30 · 점 8 로 들어오고 한글(`곡선·측점·거리·추가거리·지반고`)이 깨지지 않음.
- 테스트 — `pytest tmp/tests/ -q` **191 passed, 1 skipped** (DWG 왕복 검증 1건 추가).
### 4-3. 자리표 다듬기 — 칸 크기·그림 비율·값 미리보기 (2026-09-06 사용자 지시)
- [x] 글자 자리표에 **칸(box)** 을 둠 — 칸 한가운데에 가로·세로 가운데 맞춤으로 글자가 섬. 고른 자리표의 칸 크기를 캐드 패널에서 **가로·세로(mm)** 로 고침.
- [x] 그림 자리표는 칸을 늘려도 **비율을 지켜** 안에 들어감(남는 자리는 비움) — 로고·서명이 찌그러지지 않음.
- [x] 도각 편집 중에는 자리표에 **실제 값**을 보여 줌(공사명·용역회사·담당자·로고·서명). **저장값은 `{{키}}` 토큰 그대로** — 값이 아니라 연결을 저장한다.
- [x] 내보낸 DXF 에서도 칸이 있는 글자는 가운데 맞춤으로 나감.
#### 자체검증 (2026-09-06)
- 서버가 편집용 값을 내려주는 것 확인 — `공사명=카드미리보기_검증_sub · 용역회사=캐미팩토리 · 설계자=엄상돈 · 회사로고/서명=그림 자료`.
- 화면 — 「공사명」 자리표에 **`카드미리보기_검증_sub` 가 그대로 보임**(토큰이 아니라 값), 칸 안 가운데 정렬. 칸 크기를 60×10 → 120×24 → 200×20 으로 바꿔 즉시 반영됨.
- 그림 — 「회사로고」 자리표를 32×16 에서 **160×20 으로 늘려도 로고는 작게 가운데** 그대로(비율 유지) 확인.
- 편집을 [취소] 로 빠져나온 뒤 회사 도각 파일은 손대지 않은 상태(엔티티 51개, 토큰 유지) 확인.
- 테스트 — `pytest tmp/tests/ -q` 191 passed, 1 skipped.
### 4-4. 자리표 후속 — 마우스 크기조절·기본 도각 정렬·도면명 미리보기 (2026-09-06 사용자 지시)
- [x] **마우스로 칸 크기 조절** — 칸이 있는 글자와 그림에 네 모서리 손잡이를 붙이고, 모서리를 끌면 칸이 따라 커지고 작아짐. 캐드 본래의 「집었다 놓기」 그립은 도각 칸 맞추기에 손이 많이 가서 **끌기**를 따로 받음. 다른 도면 요소(그림)도 같은 조작을 씀.
- [x] **기본 도각(AISLO 정본)도 칸 기준 정렬** — 표제란 선에서 칸을 계산해 글자 24개 전부에 칸을 부여하고, 글자를 칸 한가운데(가로·세로)로 옮김.
- [x] **도면명·도면번호 미리보기** — 서버가 담지 않는 값(도면마다 다름)이라 편집 직전에 보던 도면의 이름·번호를 견본으로 넘김.
- [x] 복제(그립 편집)해도 미리보기 값이 유지되게 고침 — 예전에는 모서리를 한 번 만지면 자리표가 다시 `{{도면명}}` 으로 보였음.
- [x] 캐드 `index.html` 을 캐시하지 않게 함 — 빌드해도 브라우저가 옛 화면을 들고 있던 문제(검증 중 두 번 겪음).
#### 자체검증 (2026-09-06)
- 자리표 「도면명」을 놓으니 **`종단도(1)`** 로 보임(토큰이 아니라 실제 도면명), 칸 안 가운데 정렬.
- 모서리 손잡이 5개(네 모서리 + 가운데)가 화면에 뜨는 것을 픽셀로 확인.
- 오른쪽 위 모서리를 끌어 **60×10 → 90.4×20.3 mm** 로 바뀌고 패널 숫자도 따라 움직임.
- 편집을 [취소] 로 나온 뒤 회사 도각 파일은 그대로(엔티티 51개).
- 테스트 — `pytest tmp/tests/ -q` 191 passed, 1 skipped.
---
## 6. B03·B04 — 입력·전처리
### 5-1. 라이다 다중 파일 입력·병합 (2026-09-06 사용자 확정)
**지금 상태** — 지형 파일은 한 장만 받고(`정확히 1개` 검사), 전처리는 그 한 장의 점을 **통째로 메모리에 올림**(`B04_PreProcess_Engine_Structurize.py`). 실측 환산(용화 1.6GB = 4,900만점, 85점/㎡)으로 30GB 2장이면 9.2억점·메모리 29GB, 50GB 3장이면 15억점·49GB — 병합 이전에 한 장 30GB 부터 못 돌림.
**방침** — 원본은 전부 받아 그대로 두고 **병합해 전처리**함. 노선 버퍼 클리핑은 하지 않음(배수유역이 노선 밖 집수구역을 봐야 함). 대신 **좌표가 서로 멀리 떨어진 파일은 업로드에서 막음** — 다른 사업지 파일이 섞여 들어오면 합친 범위가 통째로 어긋남.
**방법** — 읽어 들이는 입구에서 **격자 대표점(0.5m 칸 최저점)만 남겨** 여러 장을 한 벌로 합침. 설계가 쓰는 격자가 1m(지면필터 2m·CSF 천 1.5m)라 0.5m 는 설계보다 촘촘해 결과 표고는 사실상 같음. 뒤 단계(지면필터·모델·등고선·배수)는 **받는 형식이 그대로**라 손대지 않음. 점이 임계(1억점) 아래면 지금처럼 전부 그대로 씀 — 작은 자료의 결과는 바뀌지 않음.
- [x] 지형 파일 개수 제한을 풂 — `정확히 1개` → `1장 이상`, 여러 장을 한 카드에 담음.
- [x] 업로드 때 각 파일 머리글의 범위만 읽어 **서로 5km 넘게 떨어지면 거부**하고 어느 파일이 떨어져 있는지 알림 (임도는 길어도 2~3km — 2026-09-06 사용자 확정).
- [x] 구조화 엔진을 **여러 파일 병합**으로 바꿈 — 합친 범위·합친 점, 겹치는 구간은 자연히 하나로 남음.
- [x] 점이 임계(1억점)를 넘으면 씨닝을 켬 — **지면 분류점은 전부 남기고** 나머지만 0.5m 칸 최저점으로 줄임. 칸 크기·임계는 설정값(`SURFACE_THIN_*`).
- [x] WF1 자동 전처리와 B04 재분석이 **프로젝트 지형 파일 전부**를 대상으로 돌게 연결함.
- [x] B03 화면에서 지형 카드에 담긴 파일 목록이 보이게 함 (「용화_서편.las 외 1장」).
#### 자체검증 (2026-09-06, 보조 워크트리 8001·5174 공용 브라우저)
용화 원본(4,900만점)을 좌·우 도엽으로 쪼갠 두 장(724MB + 865MB)을 실제 화면에서 올려 자동설계까지 완주시키고, 기존 한 장짜리 프로젝트와 수치를 맞대 봄. 씨닝은 임계를 낮춰(설정값) 강제로 켠 상태로 돌림.
- **지면점 완전 보존** — 저장 점 수는 49,006,663 → 2,491,089(5.1%)로 줄었으나 지면 분류점은 1,140,716개로 **한 점도 줄지 않음**.
- **DTM 동일** — 스무딩 전 격자 645×896의 표고가 **모든 칸에서 차이 0.0000m**. 유효 칸은 232,271 → 232,361로 가장자리 90칸이 오히려 넓어짐. 스무딩 격자만 평균 9mm 차이(99%가 5.7cm 이내, 0.1m 초과 315칸) — 넓어진 가장자리가 전역 스플라인에 퍼진 결과.
- **뒷단 결과 동일** — 노선 1,097.4m(기존 1,097m), 배수관 11개, 세부유역 11개, 자동설계 체인 「B05·B06 기본값 확정」까지 완료.
- **먼 파일 차단** — 60km 옮긴 파일을 섞으니 업로드가 400으로 막히고 화면에 「지형 파일 「다른사업지.las」의 좌표가 다른 파일과 … 떨어져 있습니다」가 뜸. 파일은 저장되지 않고 되돌려짐.
- 시험 `tmp/tests/test_b04_structurize_merge.py` 9건 — 병합·씨닝 최저점·지면점 보존·5km 경계(3km 통과·6km 거부).
### 5-2. 700줄 제한 분리 — 남은 확인
15개 분리와 초과 파일 0건 결과는 `docs/raw/plans/2026-09-04_plan_700line_split.md`에 보존했다.
- [x] B04 배수유역 라우터 쓰기 경로는 다음 재분석 시 분리 전과 결과가 같은지 확인
(2026-09-07) — **같음**. 분리 전(2026-08-30 저장분)과 분리 후(오늘 재분석)의
`00_watershed_response.json` 을 대조: **키 22개 그대로**, 빠짐·늘어남 없음,
`schema_version` 3 동일, `grid`·`flow`·`expansion` 하위 키도 그대로.
단계 산출물 7개(1차 영역 geojson·npz, 흐름방향 npz, 도로귀속 geojson·npz,
세부유역 geojson, manifest)도 모두 새로 쓰임. 분리 커밋은 `07e6441d`(2026-09-04).
### 5-3. LAS 없이 설계 — PRJ·TFW 필수 검사 오류 (2026-09-06 발견)
**증상** — 「LAS 없이 설계」를 켜면 화면은 지형 자료(포인트클라우드·좌표계·래스터)를 필수에서 빼 업로드 버튼이 열리는데, 서버가 `prj`·`tfw` 를 무조건 필수로 봐 업로드가 막힘(실행 확인: `las_free` 상태에서 노선만 올리면 `필수 입력 파일이 없습니다: prj, tfw`).
**원인** — ① 서버의 필수 목록(`_REQUIRED_FILE_TYPES`)에 「LAS 없이 설계」 예외가 없음 ② 검사 자리가 둘인데 **PRJ 를 세는 법이 다름** — 업로드 요청 때는 확장자만 봐서 노선 PRJ 를 지형 PRJ 로 오인하고, 저장 뒤 완료 검사 때는 노선/지형을 갈라 셈. 같은 파일이 한 번은 통과, 한 번은 누락으로 갈림.
- [x] LAS 없이 설계면 지형 한 벌(포인트클라우드·좌표계·래스터)을 필수에서 면제함.
- [x] 업로드 요청 검사도 완료 검사와 **같은 기준**으로 노선 PRJ 와 지형 PRJ 를 갈라 세게 통일함.
- [x] 업로드 현황의 「필수 충족」 판정이 LAS 없이 설계에서도 맞게 나오게 함 — stage 0 을 마쳤으면 충족으로 봄.
#### 자체검증 (2026-09-06)
- 공용 브라우저에서 「LAS 없이 설계」를 켜고 노선 shapefile 한 벌(shp·shx·dbf·cpg·prj)만 올려 **업로드 통과**, 도엽등고선 서피스 생성으로 진입해 지표면 모델 자동 확정까지 완료. 고치기 전에는 같은 자리에서 `필수 입력 파일이 없습니다: prj, tfw` 로 막혔음.
- 좌표계는 노선 PRJ 로 떨어지는 것을 확인(지형 PRJ 가 없을 때 `find_project_prj` 가 노선 세트 PRJ 를 집음).
- 시험 `tmp/tests/test_b03_las_free_required.py` 5건 — 노선만/CSV만 통과, 노선 PRJ 가 지형 PRJ 를 대신하지 못함, 일반 업로드는 종전대로 지형 한 벌 요구.
---
## 7. 자체검증 기록
절마다 붙은 검증 외에, 계획서 「참고」에 따로 서 있던 실측 기록들.
### 자체검증 기록 — 2026-09-06 (공용 브라우저 실측)
프로젝트 `5cff3920…` 노선 143(측점 66곳, 구조물 측점 11곳)에서 확인. 저장은 요청을
가로채 **실제로 쓰지 않고** payload 만 봤다(사용자 미저장 편집이 남아 있어서).
| 무엇 | 방법 | 결과 |
|---|---|---|
| 진입 요청 수 | 리로드하며 `/api/` 요청 집계 | B05 첫 진입 **20 → 18건**(그중 지형·지도 자료 8건, 핵심 API **10건**). B06→B05 **이동 7건** — 목표(10건 이하) 충족 |
| 중복 호출 | 같은 집계 | `auth/session` 4 → **1**, `workflow-state` 2 → **1**, 구조물 이관 POST 매 진입 → **1회만**, `sections/context`·지표면 목록 → **사라짐**(세션 캐시) |
| 카드 버튼이 서버에 안 쓰는가 | B06 첫 카드 「토사」 클릭 후 요청 감시 | **API 요청 0건**. 버튼은 즉시 활성, 세션 초안 `aislo:draft:crossdesign:{프로젝트}:143` 에 `{"0.00":{ground_type:"soil",…}}` 기록. 원래 값(리핑암)으로 되돌리고 초안도 지움 |
| [저장]이 무엇을 보내는가 | `sections/143/save` 가로채 payload 검사 | 측점 **66곳 전부** patch, 그중 **66곳에 면적 4종**(`cut/fill/cut_soil/cut_rock`), 유토곡선 포함, 사용자 조작(`revet_adjust`)도 함께. 예: 85.59m — 절토 3.912㎡ · 성토 4.8478㎡ · 토사 1.0144㎡ · 암 2.8975㎡ |
| 저장 뒤 화면 | 가로챈 뒤 상태 확인 | 잠금 오버레이 없음, 초안(`revetx`·`std-cross`) 그대로 남음 — 실패해도 조작이 사라지지 않음 |
### 자체검증 — 신규 프로젝트(용화) 2026-09-06
기존 프로젝트로는 확폭을 볼 수 없어(저장본이 기능 이전 것) **새 프로젝트를 만들어**
용화 자료(LAS 1.55GB + 지형 PRJ/TFW/TIF + 계획노선 shapefile)를 화면에서 올리고 자동설계
체인을 끝까지 돌렸다. 프로젝트 `5601e828…`, 노선 145, 연장 1,097m, 측점 67곳.
| 무엇 | 결과 |
|---|---|
| 초기값 저장(체인) | 서버 로그 `서버 재계산: route_id=145 설계 보정 0곳, 구조물 면적 15곳, 유토곡선 갱신` — **브라우저를 한 번도 안 열고** 값이 저장됨. 코리도 16.1MB 도 사전 생성. `initial_snapshot/` 생성 확인 |
| 측점 면적 | 67곳 **전부** 면적 있음(빈 곳 0) |
| 곡선부 확폭 | 31곳에 확폭. 표(별표2)와 대조 — R43.0→0.25 · R29.1→0.75 · R24.9→1.00 · R21.5→1.00 · R19.8→1.25 · R16.1→1.50 · R14.7→1.75 · R13.4→2.00. **67곳 중 64곳이 표값과 정확히 일치** |
| 확폭 테이퍼 | 나머지 3곳이 **테이퍼로만 생긴 값** — 634.0m 0.400 · 731.3m 0.066 · 1002.2m 0.194 (그 측점 반경으로는 0). 곡선 앞뒤 10m 안에 든 구조물 측점들 |
| 확폭 상한 | R12.2 측점은 표값 2.25 이지만 **유효너비 5m 상한**에 걸려 2.00 적용 — 화면 라벨 「노폭 5.00m (규격 3.00 + 확폭 2.00)」 |
| 화면 라벨 | 67개 카드 전부 노폭 라벨. 3.25 / 3.75 / 4.75 / 4.50 / 4.25 / 4.00 / 3.50 / 5.00 / 3.40 / 3.07 / 3.19 |
| 유토곡선 | 화면값 절토(자연) 5,116.3㎥ · 성토 15,397.4㎥ = 저장값(2,251.30+2,865.04=5,116.34 / 15,397.44) **일치** |
| 카드 버튼 | 「발파암」 클릭 → **서버 요청 0건**, 세션 초안에 기록 → [저장] → DB `ground_type=blasting_rock` + 면적이 브라우저 계산값으로 갱신 ✅ |
**이 과정에서 잡은 버그** — 포장·세월교 보정 헬퍼를 저장 시점으로 옮기며 `@router.post(.../save)` 데코레이터와 `save_sections` **사이에** 끼워 넣어, 라우트가 헬퍼(None 반환)에 붙어 임시저장이 500 으로 떨어지고 있었음. 헬퍼를 데코레이터 앞으로 옮겨 수정(커밋 `1cf47f39`). 화면 검증이 아니었으면 못 잡았을 자리.
**아직 못 본 것** — 구조물 측점 겹침 제거. 이 프로젝트도 구조물 측점과 20m 격자의 최소 거리가 **1.08m** 라 0.1m 겹침이 없음. 단위 테스트로만 확인.
**화면으로 확인 못 한 것 (이유 있음)**
- **구조물 측점 겹침 제거** — 이 프로젝트에서 구조물 측점과 20m 격자의 최소 거리가 **0.94m** 라 0.1m 겹침이 아예 없음. 로직은 단위 테스트(`tmp/tests/test_b05_station_merge.mjs`)로 확인.
- **곡선부 확폭·테이퍼** — 이 프로젝트 저장본이 확폭 기능 **이전에** 만들어져 측점에 `plan_radius_m`·`curve_widening_m` 이 아예 없음(0곳). 값은 **횡단을 다시 생성해야** 생기는데 그 조작은 사용자 설계를 지우므로 하지 않음. 산식은 단위 테스트 7건으로 확인. **새 프로젝트에서 확인 필요.**