# PLAN: B05/B06 종·횡단도 반응형 레이아웃 및 마커 직접 드래그 (완료 이력) ## 🎯 활성 계획: B05/B06 종·횡단도 반응형 레이아웃 및 마커 직접 드래그 (2026-07-19) ### 배경 (사용자 요청 4건) 1. B05 하단 종단도가 브라우저 폭 축소 시 높이(도면 크기)부터 줄어듦 → 하단 가로 스크롤바 방식으로 변경 2. 배치된 포인트를 3D 뷰에서 직접 드래그로 위치 변경 (0_old I-401 기능 이식, 클릭/드래그 판별 필요) 3. B06 종단도가 폭에 따라 높이가 계속 변함(가로세로비 고정) + sticky 상단 고정이 실제로 동작하지 않음 4. B06 횡단도가 화면 크기 따라 계속 확대/축소되다가 일정 폭 이하에서 우측 오버플로우 발생 ### 원인 분석 (코드 확인 완료) | 증상 | 원인 위치 | 원인 | |---|---|---| | B05 종단도 축소 | `B05_wf2_Route_UI_Style.css:68-73` | SVG에 `width/height:100%` 강제. SVG는 고정 viewBox(1200×220)의 비율을 유지(preserveAspectRatio 기본값)하므로 폭 축소 시 도면 전체가 비례 축소 | | B06 종단도 높이 변동 | `B06_wf3_ProfileCross_UI_Style.css:163-168` | `.b06-section__chart { width:100%; height:auto }` + 고정 viewBox → 폭에 비례해 높이 변동 | | B06 sticky 미동작 | `ui_template_workflow_layout.css:118-124` | `.ui-workflow-layout__main`이 `overflow:auto`이지만 높이 제한 없이 내용만큼 늘어나 실제 스크롤은 document에서 발생. sticky의 기준 스크롤포트가 "스크롤되지 않는 __main"으로 잡혀 무효화 | | B06 횡단도 오버플로우 | `B06_wf3_ProfileCross_UI_Style.css:163-168, 179-183` | `min-width:520px` + 2열 고정 grid(`repeat(2, minmax(0,1fr))`). 열 폭 < 520px이면 오버플로우. 기존 media query(900px)는 뷰포트 기준이라 사이드바 열림 상태의 실제 컨테이너 폭과 불일치 | | 마커 직접 드래그 부재 | `B05_wf2_Route_UI_Viewer.ts:176-196` | pointerdown에서 마커 히트 시 즉시 `selectObject` 후 종료. 드래그 이동 로직 없음 | ### 공통 설계: SVG 동적 크기 렌더 (사용자 합의 완료) `B06_wf3_ProfileCross_UI_Section_View.ts`의 `LONG_WIDTH`/`CROSS_WIDTH`/`LONG_HEIGHT`/`CROSS_HEIGHT` 고정 상수를 렌더 시점 파라미터로 전환한다. - `createLongitudinalProfile` / `createCrossSectionCard`에 `widthPx`/`heightPx` 파라미터 추가 (미지정 시 기존 상수 폴백 → 기존 호출부 무변경 호환) - **공통 원칙 (합의)**: 도면 높이는 항상 고정. 종단도는 폭이 최소 폭 미만이 되면 **가로 스크롤** (B05·B06 공통), 횡단도는 한눈에 봐야 하는 정보이므로 **가로 스크롤 금지** — 컨테이너 폭에 맞춰 내용 축소 - **고정 높이 (합의)**: B05 종단 = 하단 패널 컨테이너 실측 높이에 맞춤(패널 250px 내부 채움), B06 종단 = 220px, B06 횡단 = 250px - **종단도 최소 폭 산정 (합의: X축 측점 라벨 비교차 기준)**: `minWidth = PAD.left + PAD.right + 측점 수 × 라벨 슬롯 폭`. 라벨 슬롯 폭은 최장 라벨(예: "12+7.5", 10px 폰트) 실폭 + 여유로 산정 (구현 시 대략 48~56px 상수화, 검증 단계에서 실화면 확인) - 컨테이너 크기 실측은 `ResizeObserver`로 감지하고 디바운스(~150ms) 후 재도면 - **생성 시점 보장**: 최초 데이터 바인딩·패널 펼침 직후는 레이아웃 미확정으로 폭이 0일 수 있음 → 폭이 0이면 도면 생략하고, `requestAnimationFrame`+ResizeObserver 통지로 실측 폭 확정 후 도면 (B05 접힘 패널 `display:none` 상태 포함) --- ### 작업 1: B05 하단 종단도 — 높이 고정 + 하단 가로 스크롤 **방향**: 도면 폭 = `max(컨테이너 폭 - 30px(좌우 15px), 최소 도면 폭)`. 컨테이너가 좁으면 최소 폭을 유지한 채 wrapper에 `overflow-x:auto`로 하단 스크롤바 생성. 넓으면 좌우 15px 여유만 남기고 전체 사용. - 최소 도면 폭: 공통 설계의 라벨 비교차 기준 동적 산정 - 도면 높이: 하단 패널 컨테이너(`__body`) 실측 높이에 맞춤 (스크롤바 발생 시 스크롤바 높이만큼 감산) **대상 파일 및 체크리스트** - [x] `B06_wf3_ProfileCross_UI_Section_View.ts`: `createLongitudinalProfile`에 `widthPx`/`heightPx` 파라미터 추가, 내부 좌표계(x 스케일·grid·축라벨)를 파라미터 기반으로 전환, 라벨 비교차 최소 폭 산정 유틸 추가 - [x] `B05_wf2_Route_UI_Profile_Panel.ts`: ResizeObserver 부착, 실측 폭·높이 기반 `draw()` 재호출, 접힘→펼침 시 재도면, dispose 시 observer 해제 - [x] `B05_wf2_Route_UI_Style.css`: 68-73행의 `width/height:100%` 강제 제거, `__body`에 `overflow-x:auto` + 좌우 padding 15px ### 작업 2: 배치된 포인트(BP/EP/CP/AP/FP) 직접 드래그 이동 — 0_old I-401 이식 **참조 원본**: `0_old/frontend/src/RouteDesignStage.tsx:868-1012, 2097-2111` **이식할 판별 패턴** (클릭/드래그 공존의 핵심): 1. `pointerdown`: 마커 히트 시 즉시 선택하지 않고 **드래그 후보**로만 등록 (`dragStart` 좌표 기록) 2. `pointermove`: 이동 거리 3px 초과 시 드래그 확정 → `OrbitControls.enabled = false`, 지형 레이캐스트 지점으로 마커 메시 실시간 이동 (AP/FP는 반경 실린더 동반 이동) 3. `pointerup`: 드래그였으면 최종 지형 좌표로 상태 확정·저장 + OrbitControls 복원. 이동 거리 3px 이하면 기존 클릭(선택/팝오버) 동작 수행 4. `pointerleave`/`pointercancel`: 드래그 중 이탈 시 마지막 유효 좌표로 안전 종료 (원본 2097행 패턴) **대상 파일 및 체크리스트** - [x] `B05_wf2_Route_UI_Viewer.ts`: pointerdown 즉시 선택 로직(176-196행)을 3-ref(dragStart/dragCandidate/draggingMarker) 패턴으로 교체, pointermove/up/leave 핸들러 추가 - [x] `B05_wf2_Route_UI_Markers.ts`: 히트된 마커 객체→(kind, index) 식별 및 지정 마커 좌표 갱신 API 노출 (AP/FP 실린더 동기 이동 포함) - [x] `B05_wf2_Route_UI_Page.ts`: 드래그 확정 시 포인트 상태 갱신 → 기존 "재탐색 필요(stale)" 배지·확정 버튼 비활성 게이트와 연동 확인 (Epsilon 오차 보정 로직 통과 확인) - [x] 기존 팔레트 드래그&드롭(신규 배치) 및 클릭 선택·팝오버 동작 회귀 없음 확인 ### 작업 3: B06 종단도 — 높이 고정 + sticky 상단 고정 복구 **방향**: - 높이·폭: 작업 1의 동적 크기 렌더를 B06 `createSectionView`에도 적용. 높이 220px 고정, 폭은 컨테이너 전체 사용하되 **최소 폭(라벨 비교차 기준) 미만이면 B05와 동일하게 가로 스크롤** (`.b06-section__chart-wrap`의 기존 `overflow-x:auto` 활용) - sticky: B06 페이지 스코프에서 `.ui-workflow-layout__main`에 `height: calc(100vh - var(--spacing-64))`(+dvh)를 지정해 __main을 **실제 스크롤 컨테이너**로 전환 → 기존 `position:sticky; top:0`이 정상 동작 (B05가 이미 쓰는 페이지 스코프 고정 패턴과 동일, 공통 템플릿은 수정하지 않음) **대상 파일 및 체크리스트** - [x] `B06_wf3_ProfileCross_UI_Section_View.ts`: `createSectionView`의 `draw()`에 ResizeObserver 기반 실측 폭 전달 - [x] `B06_wf3_ProfileCross_UI_Page.ts`: 레이아웃 루트에 B06 스코프 클래스 부여 (예: `b06-profile-layout`) - [x] `B06_wf3_ProfileCross_UI_Style.css`: B06 스코프의 __main 높이 고정 CSS 추가, `.b06-section__chart`의 `min-width:520px` 제거(횡단은 작업 4에서 별도 처리) - [x] 검증: 횡단 리스트 스크롤 시 종단도가 상단에 고정 유지되는지, 브라우저 폭 변경 시 종단도 높이 220px 불변인지 ### 작업 4: B06 횡단도 — 크기 안정화 및 오버플로우 해소 (좌우 스크롤 금지) **방향**: 횡단 카드는 높이 **250px 고정**(합의: 기존 260 → 250) + 카드 실측 폭 기반 동적 폭 렌더. 한눈에 봐야 하는 정보이므로 가로 스크롤 없이 컨테이너 폭에 맞춰 내용을 축소. 열 수는 뷰포트가 아닌 **컨테이너 폭 기준**으로 전환해 오버플로우 자체를 제거. - grid: `repeat(2, minmax(0,1fr))` → `repeat(auto-fill, minmax(480px, 1fr))` (컨테이너 폭 < 480×2 + gap이면 자동 1열 전환, media query 의존 제거) - SVG `min-width` 제거로 카드 밖 오버플로우 원천 차단 **대상 파일 및 체크리스트** - [x] `B06_wf3_ProfileCross_UI_Section_View.ts`: `createCrossSectionCard`에 `widthPx`/`heightPx(250)` 적용 (grid 열 폭 실측 1회 → 전체 카드 공통 적용, 카드별 관찰 금지) - [x] `B06_wf3_ProfileCross_UI_Style.css`: grid `auto-fill` 전환, 카드 SVG 높이 250px 고정 - [x] 검증: 사이드바 열림/닫힘 및 브라우저 폭 축소 시 오버플로우·가로 스크롤 미발생, 카드 높이 불변 ### 공통 마무리 - [x] `prettier`(TS/CSS) 포맷 실행, 700줄 제한 확인 (Section_View 현재 533줄 → 파라미터화 후 여유 확인) - [x] 검증자 인계: 4건 통합 검증보고서 작성 대상 ### ✅ 사용자 합의 사항 (2026-07-19 문답 확정) 1. **스크롤 정책**: 종단도(B05·B06 공통)는 높이 고정 + 폭이 일정 수준 이하로 줄면 가로 스크롤. 횡단도는 가로 스크롤 금지, 컨테이너 폭에 맞춰 내용 축소(높이 고정). 2. **종단도 최소 폭**: X축 측점 라벨이 서로 교차하지 않는 기준으로 AI가 산정 (라벨 실폭 + 여유 기반). 3. **고정 높이**: B05 종단 = 하단 패널 컨테이너에 맞춤, B06 종단 = 220px, B06 횡단 = 250px.