UI 수정
기획안 반영 및 보완
This commit is contained in:
75
agent-docs/2026-06-18_DefVideo-폴더구동-UI작업.md
Normal file
75
agent-docs/2026-06-18_DefVideo-폴더구동-UI작업.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 작업 로그 — DefVideo 폴더-구동 UI & StationBar
|
||||
|
||||
- **날짜**: 2026-06-18
|
||||
- **작성 시각**: 11:29 KST
|
||||
- **작업 타이틀**: 폴더 선택 기반 데이터 구동화 + videoplayer RouteInfo 디자인 이식 + StationBar 이정 표시/레이아웃 정리
|
||||
- **대상**: `/home/hanmac/projects/gitea/b23042/DefVideo`
|
||||
- **토큰 사용량(추정)**: 오케스트레이터 input ~250k / output ~45k · 서브에이전트: Explore(파일흐름) ~40k, Explore(StationBar 감사) ~80k, DEV(폴더구동 구현) 55.5k(측정) · **총 추정 ≈ 470k tokens**
|
||||
> 정확 집계 도구가 없어 추정치(CLAUDE.md 규칙 준수).
|
||||
|
||||
---
|
||||
|
||||
## 작업 요약 (변경 파일)
|
||||
|
||||
| 파일 | 변경 |
|
||||
|------|------|
|
||||
| `client/src/utils/geoData.ts` | `parseRouteMeta()` 추가 — `<base>.route.json`/`route.json` 파싱 |
|
||||
| `client/src/types/geo.ts` | `RouteMeta/RouteInfo/RouteStructure` 타입, `FolderGeoData.routeMeta` |
|
||||
| `client/src/store/geoStore.ts` | `routeMeta` 상태 + 로드/clear |
|
||||
| `client/src/stationbar/StationBar.tsx` | 서버 fetch→geoStore, 커서 배지 10m(chain), 턴 라벨 10m·기본 100m, 구조물/터미널명 폴더화 |
|
||||
| `client/src/stationbar/components/Timeline/Timeline.tsx` | 하드코딩 신탄진/대전 제거 → props |
|
||||
| `client/src/components/overlay/RoutePanel.tsx` | 양끝 라벨 route.json 이름 |
|
||||
| `client/src/components/overlay/RouteInfoOverlay.tsx` | 좌상단 노선 배너(폴더-구동, videoplayer 디자인 이식, 폭/1920 스케일) |
|
||||
| `client/src/components/overlay/RouteInfo.module.css` | videoplayer RouteInfo 스타일 이식 |
|
||||
| `client/public/assets/title-panel-bg@2x.png` | 배너 배경 에셋 복사 |
|
||||
| `client/src/components/player/VideoPlayer.tsx` | 영상 fill(꽉 채움)+오버레이 앵커, StationBar 폴더로드 후 표시, 도구 패널 UI 숨김, 배너 추가 |
|
||||
| `client/src/hooks/useVideoPlayer.ts` | Video.js `fluid`→`fill` |
|
||||
| `client/src/index.css` | `.vjs-fill/.vjs-tech { object-fit: cover }` |
|
||||
| `client/src/App.tsx` | 좌·우 패널 UI 숨김(코드 보존, SHOW_*_PANEL) |
|
||||
| `samplevideo/하행)회덕-대전조차장.route.json` | 보조 메타데이터 샘플 생성 |
|
||||
| `agent-docs/feature-folder-load.md`, `feature-stationbar-folder-driven.md` | 설계/검수 문서 |
|
||||
|
||||
**원칙**: 모든 표시 데이터는 선택한 폴더에서 가져온다(하드코딩 0). CSV에 없는 정보(노선명·방향·시종점 역명·구조물)는 `<영상명>.route.json`으로 보완. mock/dead 코드는 삭제하지 않고 보존.
|
||||
|
||||
---
|
||||
|
||||
## 질의응답 로그 (이 세션)
|
||||
|
||||
### 환경 셋업
|
||||
- **Q. 실행방법 / 에러 있어** → Node 12 → `??` SyntaxError. nvm으로 Node 20 설치, `.nvmrc`, 의존성 재설치.
|
||||
- **Q. ecosystem 환경설정 역할 / 상대경로화** → env가 config.ts/geo로 주입됨을 설명. 절대경로→`__dirname` 기준 상대경로, ffmpeg는 PATH 명령어로 변경.
|
||||
- **Q. PM2 errored** → `ffprobe ENOENT`(FFmpeg 미설치) → 설치. 이후 false 경고는 `checkFFmpegInstalled`가 `-version` 출력을 JSON.parse 하던 버그 → exit code 확인으로 수정.
|
||||
- **Q. 한글 깨짐** → 파일명이 CP949 인코딩. iconv로 UTF-8 일괄 변환.
|
||||
- **Q. 클라이언트 빌드 실패** → Vite8(rolldown) `manualChunks` 객체→함수.
|
||||
|
||||
### 멀티 에이전트 / 진행 방식
|
||||
- **Q. 에이전트 여러 개로 속도 올릴 수 있나 / 3개(개발·문서·검수) 구성** → 서브에이전트 병렬·파이프라인 설명. 순서는 설계문서화→개발→테스트→검수로 합의, 문서·토큰 기록 병행.
|
||||
|
||||
### 폴더 선택 → 데이터 구동
|
||||
- **Q. "파일 선택"으로 영상 고르면 같은 폴더+building 데이터도 읽어 화면 세팅** → 브라우저 단일파일 보안 제약 설명, **폴더 선택(webkitdirectory) 클라이언트 처리** 방식 선택. geoData/geoStore로 폴더 파싱 구현.
|
||||
- **Q. 측점 패널 안 보임** → StationBar가 리팩터링서 누락(서버 fetch 잔존) → geoStore 연결.
|
||||
- **Q. 폴더 선택 시 업로드되나** → 아니오. webkitdirectory의 브라우저 경고 문구일 뿐, 전송 없음(전부 브라우저 메모리 파싱).
|
||||
- **Q. "파일 선택" 제거, 가운데 반투명 폴더 선택 버튼** → 적용.
|
||||
- **Q. 하단 도구 패널 collapse / 이후 UI에서 숨김** → SHOW_TOOLBAR=false(코드 보존).
|
||||
- **Q. 모든 데이터 폴더에서, 없는 건 route.json** → StationBar 완전 폴더-구동화(Explore 감사 → route.json 도입 → 구현).
|
||||
|
||||
### 좌상단 노선 배너
|
||||
- **Q. 좌상단 노선정보 표시(폴더 데이터)** → RouteInfoOverlay(이모지) 1차 → **느낌 다름** → videoplayer `RouteInfo` 디자인(배경이미지+절대좌표) 1:1 이식 → **크기 차이** → 영상폭/1920 비율 스케일 적용.
|
||||
- 연장=route.json 우선, 없으면 측점 구간 계산 / 소요=route.json 우선, 없으면 영상 길이 / 방향·노선명=route.json.
|
||||
|
||||
### 레이아웃 / 배율
|
||||
- **Q. 텍스트 배율 차이 이유** → px(+transform) vs Tailwind rem 차이. 사이니지엔 고정+화면비례 권장.
|
||||
- **Q. 퍼블리싱처럼 꽉 차게** → Video.js `fill`, object-fit cover, 오버레이 하단 앵커.
|
||||
- **Q. 좌·우 패널** → 처음 collapse → 이후 그냥 숨김(SHOW_*_PANEL=false, 코드 보존).
|
||||
|
||||
### StationBar 이정 표시
|
||||
- **Q. 커서 값 10m 단위 표현** → `chain`(연속 체이니지) + `formatMileage10`로 10m.
|
||||
- **Q. 드론 정지 시 값 고정이 실제 위치 반영?** → 맞음. 배지=프레임 GPS 투영(값), 커서 위치=시간 진행(px). 정지 시 GPS 불변→값 고정, 시간 흐름→커서 이동.
|
||||
- **Q1. 바 라벨도 폴더 데이터 파싱한 것?** → **예.** 측점선은 `측점 위경도 CSV`, 라벨/체이니지는 프레임 GPS를 측점 폴리라인에 투영한 값(둘 다 폴더 CSV 파싱). [StationBar.tsx kmLabels]
|
||||
- **Q2. 기본 100m, 실제 턴 지점은 10m** → bounds에 `turn` 플래그, 턴 라벨=`formatMileage10`(10m), 시·종점=100m. (이번 작업)
|
||||
|
||||
---
|
||||
|
||||
## 빌드/검증
|
||||
- 각 변경마다 `npm run build -w client` 통과 확인.
|
||||
- 클라이언트 전체 live `/api/geo` fetch 0건(폴더-구동). 하드코딩 노선 데이터 0건.
|
||||
108
agent-docs/2026-06-18_이슈별-해결정리.md
Normal file
108
agent-docs/2026-06-18_이슈별-해결정리.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# DefVideo — 이슈별 해결 정리
|
||||
|
||||
- **날짜**: 2026-06-18
|
||||
- **작성 시각**: 14:00 KST
|
||||
- **범위**: 폴더-구동 데이터화, RouteInfo 디자인 이식, StationBar 표시/레이아웃, 재생 커서 매끄러움
|
||||
- **공통 원칙**: 모든 표시 데이터는 선택한 폴더에서 읽는다(하드코딩 0). 폴더 CSV에 없는 정보는 `<영상명>.route.json`. mock/dead 코드는 삭제하지 않고 보존.
|
||||
|
||||
---
|
||||
|
||||
## 1. 환경: Node 12 → 실행 실패 (`Unexpected token '?'`)
|
||||
- **증상**: `npm run dev` 시 `??`(nullish) SyntaxError.
|
||||
- **원인**: 시스템 Node 12. 프로젝트는 Node 20+ 필요(Vite/better-sqlite3 등).
|
||||
- **해결**: nvm으로 Node 20 설치, `.nvmrc`(`20`) 추가, `node_modules` 재설치.
|
||||
|
||||
## 2. 환경: PM2 `errored` / `ffprobe ENOENT`
|
||||
- **원인①**: FFmpeg 미설치 → 서버가 기동 직후 죽음.
|
||||
- **원인②(버그)**: `checkFFmpegInstalled`가 `ffprobe -version`의 **텍스트** 출력을 `JSON.parse` → 항상 실패해 "FFmpeg not found" 오탐.
|
||||
- **해결**: `apt install ffmpeg`. 감지 함수를 JSON 파싱 대신 **exit code 0 확인**으로 수정(server/src/services/ffmpeg.ts). ecosystem.config.js는 `__dirname` 기준 상대경로 + PATH 명령어로 정리.
|
||||
|
||||
## 3. 한글 파일명 깨짐
|
||||
- **원인**: 파일명이 CP949(EUC-KR) 인코딩(Windows 한글). 시스템 locale UTF-8이라 `???`.
|
||||
- **해결**: `iconv`로 파일명 일괄 UTF-8 변환(내용은 멀쩡, 파일명만).
|
||||
|
||||
## 4. 클라이언트 빌드 실패 (`manualChunks is not a function`)
|
||||
- **원인**: Vite 8(rolldown)은 `manualChunks`를 **함수 형식**만 허용. 기존은 객체.
|
||||
- **해결**: `client/vite.config.ts`의 `manualChunks` 객체 → 함수로 변경.
|
||||
|
||||
---
|
||||
|
||||
## 5. 폴더 선택 → 연관 데이터 자동 로드
|
||||
- **요구**: "파일 선택"으로 영상 고르면 같은 폴더+`building/`의 측점·POI·center·srt를 함께 읽어 화면 세팅. 어떤 영상이든 동작.
|
||||
- **제약**: `<input type=file>`로 고른 단일 파일은 브라우저 보안상 형제 파일을 못 읽음.
|
||||
- **결정/해결**: **폴더 선택(webkitdirectory) + 클라이언트 파싱** 방식 채택.
|
||||
- `client/src/utils/geoData.ts`: 폴더 파일들에서 드론CSV·측점·POI·center·route.json 파싱(UTF-8/EUC-KR 처리).
|
||||
- `client/src/store/geoStore.ts`: 파싱 결과(frames/stations/pois/centerline/routeMeta/loaded) 보관.
|
||||
- 영상은 `URL.createObjectURL`로 재생. **서버 업로드/저장 없음.**
|
||||
|
||||
## 6. 측점 패널(StationBar) 안 보임
|
||||
- **원인**: 리팩터링 시 StationBar가 누락되어 여전히 서버 `/api/geo/*`를 직접 fetch → 폴더 데이터를 못 읽고, 서버 없으면 빈 화면.
|
||||
- **해결**: StationBar를 다른 컴포넌트처럼 **geoStore 구독**으로 변경(서버 fetch 제거). 클라이언트 전체 live `/api/geo` fetch 0건 확인.
|
||||
|
||||
## 7. "폴더 선택 시 업로드되나?"
|
||||
- **답**: 아니오. webkitdirectory의 **브라우저 경고 문구**일 뿐 전송 없음. 코드에 fetch/POST/FormData 0건, 전부 브라우저 메모리 파싱.
|
||||
|
||||
## 8. UI 정리: "파일 선택" 제거 / 도구·좌우 패널 숨김
|
||||
- "파일 선택" 버튼 제거, **가운데 반투명 "폴더 선택"** 버튼 추가(`VideoPlayer.tsx`).
|
||||
- 하단 도구 패널: `SHOW_TOOLBAR=false`로 **UI 숨김(코드 보존)**.
|
||||
- 좌·우 패널: `SHOW_LEFT_PANEL`/`SHOW_RIGHT_PANEL=false`로 **UI 숨김(코드 보존)**.
|
||||
|
||||
## 9. StationBar 완전 폴더-구동화 (하드코딩/mock 제거)
|
||||
- **원인**: 노선명·터미널명(신탄진/대전)·구조물·이정이 mock(`mocks/route.ts` ROUTE_LEGS, Timeline 하드코딩, segments.ts)에서 나옴.
|
||||
- **해결**:
|
||||
- `<영상명>.route.json`(routeInfo + structures) 도입 + 샘플 생성. geoStore `routeMeta`.
|
||||
- 커서 이정 배지: mock `mileageAtPx` 제거 → 폴더 데이터(드론 GPS 투영 체이니지).
|
||||
- 터미널명: route.json → 폴백 첫/끝 측점 title. Timeline 하드코딩 제거(props).
|
||||
- 구조물: route.json `structures`(없으면 POI category 폴백).
|
||||
- RoutePanel 양끝 라벨도 route.json 이름.
|
||||
|
||||
## 10. 좌상단 노선정보 배너 (videoplayer 디자인 이식)
|
||||
- **요구**: 좌상단에 방향/노선명/연장/소요 배너.
|
||||
- **과정**: 이모지 버전 → "느낌이 다름" → **videoplayer `RouteInfo` 1:1 이식**(배경이미지 `title-panel-bg@2x.png` + 절대좌표 + Noto Sans KR) → "크기 차이" → **영상 폭/1920 비율 스케일** 적용.
|
||||
- **데이터**: 연장=route.json 우선→측점 구간 계산, 소요=route.json 우선→실제 영상 길이, 방향·노선명=route.json. (`RouteInfoOverlay.tsx`, `RouteInfo.module.css`)
|
||||
|
||||
## 11. 텍스트 배율 / 화면 채움
|
||||
- **배율 질문**: 퍼블리싱(px+transform)은 텍스트 줌 무반응, DefVideo(Tailwind rem)는 반응. 사이니지엔 고정+화면비례 권장.
|
||||
- **꽉 채움**: Video.js `fluid`→`fill`, `.vjs-fill/.vjs-tech { object-fit: cover }`, 오버레이 하단 앵커 → 검은 여백/하단 잘림 해결(`VideoPlayer.tsx`, `useVideoPlayer.ts`, `index.css`).
|
||||
|
||||
---
|
||||
|
||||
## 12. 커서 이정 10m 단위 표시
|
||||
- **원인**: 배지가 최근접 측점 km(100m 양자화). `formatMileage`도 100m 반올림.
|
||||
- **해결**: 연속 체이니지(`chain`) + `formatMileage10`(10m). 바 라벨은 **턴 지점만 10m**, 시·종점 등 기본은 100m.
|
||||
|
||||
## 13. 초반 감소구간이 주황(증가)으로 표시
|
||||
- **원인**: 구간 방향 계산이 시작 방향을 `dir=1`(증가)로 가정.
|
||||
- **해결**: 첫 유의미 이동(±100m)으로 **시작 방향을 실데이터 판정** → 감소면 하늘색.
|
||||
|
||||
## 14. 시설물(구조물) 표시 = videoplayer 아이콘
|
||||
- **요구**: 텍스트만 → videoplayer 3-slice 아이콘으로, 재생바 위에 겹쳐, 통과 시 색 변경.
|
||||
- **해결**:
|
||||
- DefVideo에 이미 있던 `RouteSegment`(3-slice PNG, `public/assets/route-segment/`) 사용.
|
||||
- 아이콘을 **재생바 위 레이어(z-index)** 에 배치(top≈바 중앙).
|
||||
- **원본 치수 적용**: 교량 22×16/cap6/top32.5, 터널 24×14/cap10/top33.75.
|
||||
- 라벨 폰트 **Noto Sans KR** 추가(`index.html`) — 글자 스타일 원본 일치.
|
||||
- 커서가 아이콘 **좌측 끝에 닿는 순간** `passed`로 전환 → **아이콘+라벨 색 동시 변경**(neutral→accent).
|
||||
|
||||
---
|
||||
|
||||
## 15. 재생 커서가 끊김/물결/흔들림 (핵심 성능 이슈)
|
||||
단계적으로 진단·수정:
|
||||
|
||||
1. **끊김(250ms 점프)**: `timeupdate`(250ms)로만 갱신 → rAF 도입.
|
||||
2. **물결(출렁임)**: rAF 60fps로 `currentTime()`을 읽는데 미디어 시계는 29.97fps → 같은값→점프 반복. `requestVideoFrameCallback` 시도(여전히 프레임 단위 계단).
|
||||
3. **흔들림 잔존(최종 해결)**: 원인 = **4K HEVC 디코딩 + 60fps React 리렌더 경쟁 + 보간 역방향 보정**.
|
||||
- **단조(monotonic) 보간**: 벽시계 기반 보간, 작은 역행 무시(흔들림 제거), 뒤처짐/시크만 재동기화(`VideoPlayer.tsx`).
|
||||
- **커서/진행바를 React에서 분리**: 라이브 시간(`smoothTimeRef`)을 StationBar가 rAF로 읽어 **CSS 변수(`--cursor-x`,`--pos-px`)만 직접 갱신**. React 상태는 배지/색용으로 ~10fps throttle.
|
||||
- 커서 `left`→**`transform: translateX`**(GPU 합성, reflow 제거) + `will-change`. 진행바 폭은 CSS 변수 `calc()`.
|
||||
- 결과: 합성 스레드에서 부드럽게 이동, 4K 디코딩 부하와 분리. **해결 확인됨.**
|
||||
- **참고**: 비교 대상 videoplayer는 실제 영상이 없는 순수 애니메이션(17px/s)이라 본래 매끄러움. DefVideo는 실영상 동기라 위 최적화가 필요했음.
|
||||
|
||||
---
|
||||
|
||||
## 사용자 목적 적합성 확인
|
||||
- "접속자가 자기 영상 폴더를 올려 재생" → **클라이언트 방식이라 사용자별 독립·무업로드로 적합.**
|
||||
- 실전 점검: ① **HEVC 코덱 호환성(최우선)** ② 폴더 파일명 규칙 ③ CSV 인코딩.
|
||||
|
||||
## 토큰 사용량(추정)
|
||||
- 본 세션 누적 ≈ **600k+ tokens** (오케스트레이터 + 서브에이전트 Explore×2/DEV×1). 정확 집계 도구 없어 추정.
|
||||
358
agent-docs/feature-folder-load.md
Normal file
358
agent-docs/feature-folder-load.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# 기능: 폴더 선택 기반 영상+데이터 동시 로딩
|
||||
|
||||
> 설계 문서 (① 설계 단계). 이 문서는 구현 지시서이며, 코드 변경은 ②구현 단계 에이전트가 수행한다.
|
||||
> 모든 경로는 프로젝트 루트 `/home/hanmac/projects/gitea/b23042/DefVideo` 기준 상대경로로 표기한다.
|
||||
|
||||
---
|
||||
|
||||
## 목표 / 사용자 시나리오
|
||||
|
||||
영상과 지리정보(드론 GPS/자세, 측점, POI, 선로 중심선)를 **하나의 폴더 선택으로 동시에 로드**한다.
|
||||
현재는 영상은 클라이언트 File API로, 지리정보는 서버가 고정 디렉토리(`samplevideo`) + 하드코딩 영상명(`'회덕'`)으로 따로 로드하므로 두 데이터가 연결되지 않는다.
|
||||
|
||||
### 사용자 시나리오
|
||||
1. 사용자가 영상 플레이어에서 **"폴더 선택"** 컨트롤을 클릭한다.
|
||||
2. OS 폴더 선택 다이얼로그에서 영상 + 데이터가 들어있는 폴더(예: `samplevideo/`)를 고른다.
|
||||
3. 브라우저가 `webkitdirectory`로 폴더 내 모든 파일(하위 `building/` 포함)을 노출한다.
|
||||
4. 클라이언트가:
|
||||
- 폴더에서 영상 파일(`.mp4/.MP4/.webm`)을 찾아 `loadLocalFile`로 재생한다.
|
||||
- `<base>.csv`(드론 프레임), `center.csv`(중심선), `building/<base>_POI_위경도값.csv`, `building/<base>_측점_위경도값.csv`를 **클라이언트에서 직접 파싱**해 Zustand 지리정보 스토어에 적재한다.
|
||||
5. 우측 패널(지리정보/측점)과 영상 오버레이(측점/POI/중심선)가 **선택한 폴더의 데이터** 기준으로 즉시 표시된다.
|
||||
6. **영상명에 의존하지 않는다**: `'회덕'` 하드코딩 제거. 동일한 폴더 구조면 어떤 영상이든 동작한다.
|
||||
|
||||
### 폴백
|
||||
- 폴더가 아직 선택되지 않았으면 지리정보 스토어는 비어 있다. 각 소비 컴포넌트는 빈 상태(측점 0개, 검색 결과 없음, 오버레이 미표시)를 표시한다.
|
||||
- (선택) 스토어가 비었을 때 기존 `/api/geo/*` 서버 API로 폴백할 수 있으나, **기본 동작은 클라이언트 스토어 우선**. 이 문서에서는 서버 폴백을 "옵션"으로 두고, 기본 구현은 스토어 단일 소스로 한다.
|
||||
|
||||
---
|
||||
|
||||
## 제약
|
||||
|
||||
- **브라우저 폴더 접근**: `<input type="file" webkitdirectory>` 사용. Chrome/Firefox/Edge(PC) 지원 — 프로젝트 대상 브라우저와 일치(CLAUDE.md). iOS Safari 비대상.
|
||||
- 선택된 각 `File`은 `file.webkitRelativePath`(예: `samplevideo/building/...csv`)로 폴더 내 상대경로를 노출한다.
|
||||
- 첫 세그먼트는 사용자가 고른 루트 폴더명이므로, 매칭 시 **basename + 마지막 디렉토리 세그먼트**로 판단한다.
|
||||
- **클라이언트 전용**: 서버 의존 없음. 파일 업로드 없이 File API로 직접 처리.
|
||||
- **인코딩**: POI/측점 CSV와 `center.csv`는 **EUC-KR**(서버는 `iconv-lite`로 디코딩). 클라이언트에는 `iconv-lite`가 없으므로 브라우저 **`TextDecoder('euc-kr')`**(Encoding API legacy label, Chrome/FF/Edge 지원)로 디코딩한다. 드론 `<base>.csv`는 **UTF-8 + BOM**.
|
||||
- **메모리**: 영상은 `URL.createObjectURL()`로 직접 재생, 사용 후 `revokeObjectURL`(기존 `useVideoPlayer.loadLocalFile`가 이미 처리).
|
||||
- **좌표 투영**: 클라이언트는 이미 `proj4`(EPSG:5186) 기반 투영 유틸을 보유. 서버 `geoMatch.ts`는 단순 ENU(cos-lat 근사) 방식이라 투영식이 다름 — **데이터 로딩/파싱/검색 로직만 포팅**하고 투영은 기존 클라이언트 유틸(`geoProjection.ts`)을 재사용한다(아래 상세).
|
||||
|
||||
---
|
||||
|
||||
## 파일 명명 규칙 & CSV 스키마
|
||||
|
||||
`<base>` = 영상 파일명에서 확장자를 제거한 이름. 예: `하행)회덕-대전조차장.MP4` → base `하행)회덕-대전조차장`.
|
||||
|
||||
```
|
||||
<folder>/
|
||||
<base>.MP4 (또는 .mp4/.webm) → 영상 (loadLocalFile / createObjectURL)
|
||||
<base>.csv → 드론 프레임 (UTF-8 BOM)
|
||||
<base>.srt → 프레임 메타(현재 geo 시스템 미파싱 — 주의)
|
||||
center.csv → 중심선 (224행, EUC-KR)
|
||||
building/<base>_POI_위경도값.csv → POI 건물 (EUC-KR)
|
||||
building/<base>_측점_위경도값.csv → 측점 (EUC-KR)
|
||||
building/<base>_POI_XY값.csv → 미사용
|
||||
building/<base>_측점_XY값.csv → 미사용
|
||||
```
|
||||
|
||||
### 1) 드론 프레임 `<base>.csv` (UTF-8, BOM 있음)
|
||||
헤더(BOM 제거 후): `frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len`
|
||||
```
|
||||
frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len
|
||||
0,36.402256,127.421515,84.244,114.7,-29.8,0.0,24.00
|
||||
1,36.402256,127.421515,84.244,114.7,-29.8,0.0,24.00
|
||||
```
|
||||
- 컬럼은 **헤더 이름으로 인덱스 조회**(`header.indexOf('frame_cnt')` 등). 서버 `geoMatch.ts:290-302`와 동일.
|
||||
- 매핑: `frame=frame_cnt(int)`, `lat=latitude`, `lon=longitude`, `altitude`, `yaw`, `pitch`, `roll`, `focalLen=focal_len`.
|
||||
- `!isNaN(lat)` 행만 채택.
|
||||
- 구분자 `,`. 따옴표 처리는 `parseCsvLine` 동일 규칙.
|
||||
|
||||
### 2) 중심선 `center.csv` (EUC-KR — 헤더 한글이 깨지므로 **위치 인덱스 직접 사용**)
|
||||
헤더(깨진 상태): `id,lat,lon,표고(H),지오이드높이(N),타원체고(h),67.39,...,x,y`
|
||||
```
|
||||
id,lat,lon,<EUC-KR깨짐>,<깨짐>,<깨짐(h)>,67.39,41.57...,,x,y
|
||||
0,36.4087069,127.4256135,44.5959549,25.449,70.0449549,2.6549549,...,238176.0008,423480.0717
|
||||
```
|
||||
- 서버 `geoMatch.ts:251-257` 규칙: **인덱스 1=lat, 2=lon, 5=타원체고(h)=z**. 헤더 이름 의존 금지(EUC-KR 깨짐 회피).
|
||||
- `!isNaN(lat&&lon&&z)` 행만 채택. 헤더 1행 skip.
|
||||
|
||||
### 3) POI `building/<base>_POI_위경도값.csv` (EUC-KR)
|
||||
헤더: `title,category_clean,address_road,address_jibun,lat,lon,z`
|
||||
```
|
||||
title,category_clean,address_road,address_jibun,lat,lon,z
|
||||
한국철도공사대전철도역사,역사,대전 ...,대전 ...,36.371101,127.421776,14.93833585
|
||||
대전역,역사,...,...,36.380398,127.422422,17.43774457
|
||||
```
|
||||
- 매핑(헤더 이름 조회, `geoMatch.ts:323-335`): `title`, `category = category_clean || '건물'`, `lat`, `lon`, `z = z || 0`, `type='poi'`.
|
||||
- 파일 선택 우선순위(`geoMatch.ts:317-320`): 파일명에 `타원체고` 포함 버전이 있으면 그것 우선, 없으면 `타원체고` 미포함 버전. (현재 샘플엔 `타원체고` 파일 없음 → 기본 `_POI_위경도값.csv` 사용.)
|
||||
- `category_clean` 값도 EUC-KR(터널/교량/역사/지장물). `TextDecoder('euc-kr')`로 정상 복원돼야 RoutePanel 카테고리 필터/이모지가 동작한다.
|
||||
|
||||
### 4) 측점 `building/<base>_측점_위경도값.csv` (EUC-KR)
|
||||
헤더: `title,category_clean,address_road,address_jibun,lat,lon,z` (POI와 동일)
|
||||
```
|
||||
title,category_clean,address_road,address_jibun,lat,lon,z
|
||||
157K900,,,,36.40296455,127.4218765,0.023576703
|
||||
158K000,,,,36.40222619,127.4212379,-0.148290668
|
||||
```
|
||||
- 매핑(`geoMatch.ts:345-355`): `title`, `category='측점'`(강제), `lat`, `lon`, `z = z || 0`, `type='station'`.
|
||||
- `title`은 `157K900` 형태(km 정렬에 사용: 정규식 `/(\d+)[Kk](\d+)/`).
|
||||
|
||||
### 5) SRT `<base>.srt` (UTF-8) — 참고용, 현재 geo 미사용
|
||||
```
|
||||
1
|
||||
00:00:00,000 --> 00:00:00,033
|
||||
FrameCnt: 0 2026-03-05 17:18:28.405
|
||||
[... focal_len: 24.00] [latitude: 36.402256] [longitude: 127.421515] [rel_alt: 16.853 abs_alt: 84.244] [gb_yaw: 114.7 gb_pitch: -29.8 gb_roll: 0.0] ...
|
||||
```
|
||||
- 서버 `geoMatch.ts:208-222 loadTerrainOffset`만 SRT를 읽어 `terrain offset = abs_alt - rel_alt`를 계산하나, **현재 코드 경로에서 이 offset은 어디에도 적용되지 않는다**(`_terrainOffset`은 set만 되고 사용처 없음). → 이번 포팅에서 SRT는 **파싱하지 않아도 무방**(현 동작 동일). 문서상 명시.
|
||||
|
||||
---
|
||||
|
||||
## 현재 구조 (As-Is)
|
||||
|
||||
### 영상 선택
|
||||
- `client/src/components/player/VideoPlayer.tsx:194-205` — "파일 선택" 라벨 + `<input type="file" accept="video/*">`, `onChange`에서 `loadLocalFile(file)` 호출.
|
||||
- 드래그앤드롭: `VideoPlayer.tsx:96-100`.
|
||||
- `client/src/hooks/useVideoPlayer.ts:63-81 loadLocalFile` — `URL.createObjectURL(file)` → `player.src(...)` → `store.setSource({kind:'local', file, objectUrl})` → `emptied` 시 `revokeObjectURL`.
|
||||
- 소스 타입: `client/src/types/player.ts:1-3 VideoSource`.
|
||||
|
||||
### 지리정보 (서버 단일 소스)
|
||||
- 데이터 디렉토리: `server/src/routes/geo.ts:8-11` — `GEO_DATA_DIR = process.env.GEO_DATA_DIR || <root>/samplevideo`, `setGeoDataDir()` 호출.
|
||||
- 서버 서비스 `server/src/services/geoMatch.ts` (싱글턴 캐시 `_frames/_pois/_centerline`):
|
||||
- CSV 파서 `parseCsvLine`(162-173), `readCsvUtf8`(175-192, EUC-KR/UTF-8 BOM 자동 판별, `iconv-lite`).
|
||||
- 투영: `geoToEnu`(78-87, cos-lat 근사 ENU), `projectEnu`(90-124), `project3D`(127-156).
|
||||
- 로더: `loadFrames`(275-305), `loadPois`(307-361), `loadCenterline`(236-261).
|
||||
- ⚠ `loadFrames:285` — `f.includes('회덕')` **하드코딩**. (제거 대상)
|
||||
- `loadCenterline:239` — `CENTER_CSV_PATH` 또는 `pythonsource/input/center.csv` 경로(샘플 폴더의 center.csv와 별개일 수 있음).
|
||||
- 원점: `getWorldOrigin`(378-387) — 첫 측점(`stationOrder` 정렬) 사용, 없으면 frame[0].
|
||||
- 검색 API: `findFramesForPoi`(399-468), `findPoisForFrame`(473-512), `getAllPois`(515-517), `getDroneFrames`(520-522), `getCenterlinePoints`(263-265).
|
||||
- HTTP 라우트 `server/src/routes/geo.ts`:
|
||||
- `GET /api/geo/pois`(14-21) → `getAllPois()` → `GeoPoint[]`
|
||||
- `GET /api/geo/search?q=&margin=&maxDist=&yawOffset=`(27-45) → `{poi, frames: FrameMatch[]}`
|
||||
- `GET /api/geo/frame/:frameNum?margin=&yawOffset=`(51-64) → `{droneFrame, pois: PoiInFrame[]}`
|
||||
- `GET /api/geo/frames?step=`(70-79) → 샘플링된 `DroneFrame[]`
|
||||
- `GET /api/geo/centerline`(85-92) → `CenterlinePoint[]`
|
||||
|
||||
### 클라이언트 투영 (이미 존재 — 재사용)
|
||||
- `client/src/utils/geoProjection.ts`:
|
||||
- `proj4` EPSG:5186 정의(70-72), `latLonToTM`(76-80), `geoToEnu`(86-92, TM 기반).
|
||||
- `CameraParams` / `DEFAULT_CAMERA_PARAMS`(35-62), `paramsFromFrame`(65-67).
|
||||
- `toCameraCoords`(175-184), `pixelFromCamera`(115-126), `projectPoint`(201-294).
|
||||
- **오버레이(StationOverlay/RoutePanel)는 이 유틸로 자체 투영**한다. 서버 `project3D`와 다른 알고리즘. → 검색용 `findFramesForPoi`/`findPoisForFrame`만 서버에 의존.
|
||||
|
||||
### 클라이언트 소비 컴포넌트 (각 fetch 정리)
|
||||
| 컴포넌트 | fetch | 기대 응답 형태 | 용도 |
|
||||
|---|---|---|---|
|
||||
| `GeoSearch.tsx:61` | `GET /api/geo/pois` | `GeoPoint[]` | 자동완성 목록 |
|
||||
| `GeoSearch.tsx:82` | `GET /api/geo/search?q=&margin=1.0&maxDist=1500` | `{poi:GeoPoint, frames:FrameMatch[]}` | 건물→프레임 검색 |
|
||||
| `GeoSearch.tsx:98` | `GET /api/geo/frame/{currentFrame}?margin=1.0` | `{pois:PoiInFrame[]}` | 프레임→건물 역조회 |
|
||||
| `StationVerify.tsx:57` | `GET /api/geo/pois` | `GeoPoint[]` (station만 필터) | 측점 목록 |
|
||||
| `StationVerify.tsx:69` | `GET /api/geo/frames?step=1` | `{frame,lat,lon}[]` | GPS 최근접 프레임 |
|
||||
| `StationVerify.tsx:102` | `GET /api/geo/search?q=&margin=1.2&maxDist=2000` | `{poi,frames:FrameMatch[]}` | 검증 정보 |
|
||||
| `StationOverlay.tsx:186` | `GET /api/geo/pois` | `GeoPoint[]` (station/poi 분리) | 라벨 사전계산 |
|
||||
| `StationOverlay.tsx:197` | `GET /api/geo/centerline` | `CenterlinePoint[]` | 중심선 |
|
||||
| `StationOverlay.tsx:206` | `GET /api/geo/frames?step=1` | `DroneFrameBasic[]` | 드론 프레임 |
|
||||
| `RoutePanel.tsx:75` | `GET /api/geo/pois` | `GeoPoint[]` | 미니맵 측점/POI |
|
||||
| `RoutePanel.tsx:87` | `GET /api/geo/frames?step=1` | `DroneFrameBasic[]` | 현재 km 계산 |
|
||||
|
||||
### 상태/배선
|
||||
- `client/src/App.tsx` — 우측 탭 `rightTab: 'annotation'|'geo'|'station'`(19), GeoSearch(151-157)/StationVerify(158-163) 렌더. `currentFrame = round(currentTime*fps)`(35).
|
||||
- `client/src/store/playerStore.ts` — 영상 재생 상태(source/currentTime/fps 등). 지리정보 스토어 없음.
|
||||
- `playerRef.loadServerStream`은 좌측 VideoList에서, `loadLocalFile`은 VideoPlayer 내부 input/드롭에서 호출.
|
||||
|
||||
---
|
||||
|
||||
## 변경 설계 (To-Be)
|
||||
|
||||
### 개요
|
||||
1. 폴더 선택 UI → 폴더 내 파일 목록(`File[]` + `webkitRelativePath`)을 단일 파서 진입점에 넘긴다.
|
||||
2. 새 **클라이언트 지리정보 스토어**(`geoStore`)가 파싱 결과(frames/pois/stations/centerline/origin)를 보관.
|
||||
3. CSV 파싱 + 서버 `geoMatch` 검색 로직을 클라이언트 유틸로 포팅(투영은 기존 `geoProjection.ts` 재사용).
|
||||
4. 4개 소비 컴포넌트의 `/api/geo/*` fetch → `geoStore` 셀렉터 + 포팅 함수 호출로 교체.
|
||||
5. 영상 파일은 폴더에서 찾아 기존 `loadLocalFile`로 재생.
|
||||
|
||||
---
|
||||
|
||||
### New: 폴더 선택 UI
|
||||
**파일**: `client/src/components/player/VideoPlayer.tsx` 수정.
|
||||
|
||||
- 기존 "파일 선택"(194-205) 옆에 **"폴더 선택"** 라벨+input 추가:
|
||||
```tsx
|
||||
<label className="cursor-pointer bg-emerald-600 hover:bg-emerald-700 ... rounded">
|
||||
폴더 선택
|
||||
<input type="file" className="hidden"
|
||||
// @ts-expect-error - 비표준 속성
|
||||
webkitdirectory="" directory="" multiple
|
||||
onChange={(e) => { const files = e.target.files; if (files?.length) onSelectFolder(Array.from(files)); }} />
|
||||
</label>
|
||||
```
|
||||
- `webkitdirectory` 타입 보강: `client/src/types/`에 전역 선언 추가하거나 `// @ts-expect-error`. (권장: `client/src/vite-env.d.ts` 또는 새 `client/src/types/dom.d.ts`에 `HTMLInputElement` 확장 선언.)
|
||||
- `onSelectFolder(files: File[])` 핸들러(VideoPlayer 내부 또는 App에서 전달):
|
||||
1. `loadFolderGeoData(files)` 호출 → 파싱 + `geoStore` 채움(영상 File도 함께 반환/식별).
|
||||
2. 영상 File을 찾으면 `loadLocalFile(videoFile)`.
|
||||
- 기존 "파일 선택"(단일 영상)은 **유지**(지리정보 없이 영상만).
|
||||
|
||||
---
|
||||
|
||||
### New: 클라이언트 지리정보 스토어 (Zustand)
|
||||
**신규 파일**: `client/src/store/geoStore.ts`
|
||||
|
||||
```ts
|
||||
import { create } from 'zustand';
|
||||
// 타입은 client/src/types/geo.ts(신규)로 분리 권장
|
||||
export interface DroneFrame { frame:number; lat:number; lon:number; altitude:number; yaw:number; pitch:number; roll:number; focalLen:number; }
|
||||
export interface GeoPoint { title:string; category:string; lat:number; lon:number; z:number; type:'poi'|'station'; }
|
||||
export interface CenterlinePoint { lat:number; lon:number; z:number; }
|
||||
|
||||
interface GeoStore {
|
||||
loaded: boolean;
|
||||
frames: DroneFrame[];
|
||||
pois: GeoPoint[]; // type==='poi'
|
||||
stations: GeoPoint[]; // type==='station' (stationOrder 정렬)
|
||||
centerline: CenterlinePoint[];
|
||||
origin: { lat:number; lon:number; alt:number } | null; // 첫 측점 = ENU 원점
|
||||
baseName: string | null;
|
||||
setGeoData: (d: Partial<GeoStore>) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
export const useGeoStore = create<GeoStore>((set) => ({ /* 초기값 + setGeoData/reset */ }));
|
||||
```
|
||||
- `pois`/`stations`는 `getAllPois()` 결과를 `type`으로 분리해 저장(소비처 편의). 또는 통합 `pois` 하나만 두고 셀렉터로 분리해도 됨 — **분리 저장 권장**(현 컴포넌트들이 매번 filter 함).
|
||||
- `origin` = 첫 측점(`stationOrder` 최소). 서버 `getWorldOrigin` 동치.
|
||||
|
||||
---
|
||||
|
||||
### New: 클라이언트 CSV 파싱 + geoMatch 포팅
|
||||
**신규 파일**: `client/src/utils/geoData.ts` (파싱 + 로딩 + 검색 포팅)
|
||||
|
||||
#### 인코딩/파싱 헬퍼 (서버 `readCsvUtf8`/`parseCsvLine` 포팅)
|
||||
- `parseCsvLine(line)` — 서버 `geoMatch.ts:162-173` 그대로 포팅(순수 함수, 의존성 없음).
|
||||
- `decodeBytes(buf: ArrayBuffer, encoding:'utf-8'|'euc-kr')` — `new TextDecoder(encoding).decode(buf)`. UTF-8 BOM 제거(`replace(/^/,'')`). EUC-KR은 `TextDecoder('euc-kr')`.
|
||||
- 드론 csv: UTF-8(BOM). POI/측점/center: EUC-KR.
|
||||
- 서버처럼 자동판별(`iconv`)이 아니라 **파일 종류별로 인코딩 지정**(폴더 구조 알고 있으므로 단순). center/POI/측점은 EUC-KR 고정.
|
||||
- `readCsv(file: File, encoding)` → `Promise<string[][]>` — `await file.arrayBuffer()` → decode → `split(/\r?\n/).filter(Boolean).map(parseCsvLine)`.
|
||||
|
||||
#### 로더 (서버 loader 포팅, but 파일은 `File[]`에서 매칭)
|
||||
- `findVideoFile(files)` — 확장자 `.mp4/.webm`(대소문자 무시), `building/` 미포함. → base 추출.
|
||||
- `parseDroneFrames(file): Promise<DroneFrame[]>` — 서버 `loadFrames:289-302` 로직(헤더 이름 인덱스). **`회덕` 필터 제거**, base 일치 또는 `building/` 미포함 루트 csv로 식별.
|
||||
- `parsePois(poiFile, stationFile): Promise<GeoPoint[]>` — 서버 `loadPois:321-357` 포팅. POI=`category_clean||'건물'`,`type:'poi'`; 측점=`category:'측점'`,`type:'station'`. 파일 선택은 `building/` 내 `webkitRelativePath`에서 `POI`+`위경도`, `측점`+`위경도` 매칭(`타원체고` 우선규칙 유지).
|
||||
- `parseCenterline(file): Promise<CenterlinePoint[]>` — 서버 `loadCenterline:253-257` 포팅(인덱스 1/2/5, EUC-KR).
|
||||
- `loadFolderGeoData(files: File[])` — 위 파서들을 호출해 `geoStore.setGeoData({...})` 채우고, 영상 File을 반환.
|
||||
|
||||
#### 검색 함수 포팅 (서버 `geoMatch.ts` → 클라이언트)
|
||||
검색은 **서버의 단순 ENU `project3D`(geoMatch.ts) 알고리즘을 그대로 포팅**해 서버 응답과 동일 결과를 보장한다(오버레이용 `geoProjection.ts`와는 별개 — 검색 UI 호환 우선).
|
||||
| 포팅 함수 | 서버 소스 라인 | 비고 |
|
||||
|---|---|---|
|
||||
| `toRad` | `geoMatch.ts:63` | |
|
||||
| `geoToEnu` (cos-lat ENU) | `geoMatch.ts:78-87` | 검색 전용. 오버레이는 기존 proj4 ENU 사용 |
|
||||
| `projectEnu` | `geoMatch.ts:90-124` | |
|
||||
| `project3D` | `geoMatch.ts:127-156` | |
|
||||
| `stationOrder` | `geoMatch.ts:371-375` | (이미 컴포넌트들에 중복 존재 — 공용화) |
|
||||
| `getWorldOrigin` | `geoMatch.ts:378-387` | 스토어 `origin`으로 대체 가능 |
|
||||
| `findFramesForPoi(query, margin, maxDist, yawOffset)` | `geoMatch.ts:399-468` | 스토어 frames/pois 입력으로 변경 |
|
||||
| `findPoisForFrame(frameNum, margin, yawOffset)` | `geoMatch.ts:473-512` | 동일 |
|
||||
- 입력을 싱글턴 캐시(`loadFrames()`) 대신 **`geoStore` 데이터를 인자/직접 read**로 변경. 반환 타입은 서버와 동일(`FrameMatch`/`PoiInFrame`) → 컴포넌트 인터페이스 무변경.
|
||||
- `DEFAULT_FPS=30`, `GAP=30` 등 상수 동일 포팅.
|
||||
|
||||
---
|
||||
|
||||
### Changes: 소비 컴포넌트 (fetch → 스토어)
|
||||
각 컴포넌트는 내부 `interface GeoPoint/FrameMatch` 중복 선언을 **`types/geo.ts`로 통합**(선택, 최소변경 시 유지 가능). 핵심은 fetch 제거:
|
||||
|
||||
- **`GeoSearch.tsx`**
|
||||
- `:61` `fetch('/api/geo/pois')` → `useGeoStore` 의 `[...stations, ...pois]`.
|
||||
- `:82` `/api/geo/search` → 포팅 `findFramesForPoi(q, 1.0, 1500, 0)` 직접 호출(동기). `loading`/`error`는 데이터 없을 때만 의미.
|
||||
- `:98` `/api/geo/frame/{n}` → 포팅 `findPoisForFrame(currentFrame, 1.0)`.
|
||||
- **`StationVerify.tsx`**
|
||||
- `:57` pois → 스토어 `stations`.
|
||||
- `:69` frames(`{frame,lat,lon}`) → 스토어 `frames`(필드 일부만 사용).
|
||||
- `:102` search → 포팅 `findFramesForPoi(title, 1.2, 2000)`.
|
||||
- **`StationOverlay.tsx`**
|
||||
- `:186` pois → 스토어 `stations`/`pois`(이미 station/poi 분리 사용). `updateWorldOrigin`은 스토어 `origin`으로 대체.
|
||||
- `:197` centerline → 스토어 `centerline`.
|
||||
- `:206` frames → 스토어 `frames`.
|
||||
- 투영은 기존 `geoProjection.ts` 그대로(변경 없음).
|
||||
- **`RoutePanel.tsx`**
|
||||
- `:75` pois → 스토어. `:87` frames → 스토어.
|
||||
- **폴백/가드**: 스토어 `loaded===false`이면 빈 배열로 동작(현재 fetch 실패 시와 동일한 빈 상태). `useEffect`의 `[visible]` 의존성은 스토어 구독으로 대체(데이터 도착 시 자동 재렌더).
|
||||
- (옵션) 스토어가 비어 있을 때 기존 `/api/geo/*`로 폴백하려면 각 컴포넌트에 `if (store.loaded) useStore else fetch` 분기. **기본 구현은 스토어 단일 소스 권장**(서버 의존 제거가 목표).
|
||||
|
||||
---
|
||||
|
||||
### 영상 재생 배선
|
||||
- `loadFolderGeoData`가 반환한 영상 `File` → `playerRef.loadLocalFile(file)`(기존 경로 그대로).
|
||||
- 영상이 폴더에 없으면 지리정보만 로드(오버레이는 동작하나 재생 화면 없음) — 에러 토스트/콘솔 경고.
|
||||
- `App.tsx`: 폴더 선택 핸들러를 VideoPlayer로 내려보내거나 VideoPlayer 내부에서 `loadLocalFile`(이미 보유) + `loadFolderGeoData` 직접 호출. **VideoPlayer 내부 처리 권장**(이미 `loadLocalFile` 접근 가능).
|
||||
|
||||
---
|
||||
|
||||
## 구현 태스크 목록
|
||||
|
||||
- [ ] `client/src/types/geo.ts` 신규 — `DroneFrame`/`GeoPoint`/`CenterlinePoint`/`FrameMatch`/`PoiInFrame` 공용 타입.
|
||||
- [ ] `client/src/store/geoStore.ts` 신규 — Zustand 스토어(frames/pois/stations/centerline/origin/baseName/loaded + setGeoData/reset).
|
||||
- [ ] `client/src/utils/geoData.ts` 신규:
|
||||
- [ ] `parseCsvLine` 포팅(server `geoMatch.ts:162-173`).
|
||||
- [ ] `decodeBytes`/`readCsv`(TextDecoder utf-8 + euc-kr, BOM 제거).
|
||||
- [ ] `findVideoFile(files)` (mp4/MP4/webm, building 제외, base 추출).
|
||||
- [ ] `parseDroneFrames`(헤더 인덱스, **회덕 하드코딩 없음**).
|
||||
- [ ] `parsePois`(POI: category_clean||건물 / 측점: '측점', 타원체고 우선규칙).
|
||||
- [ ] `parseCenterline`(인덱스 1/2/5, EUC-KR).
|
||||
- [ ] `loadFolderGeoData(files)` → setGeoData + return videoFile.
|
||||
- [ ] `client/src/utils/geoSearch.ts` 신규(또는 geoData.ts 내) — `toRad/geoToEnu/projectEnu/project3D/stationOrder/findFramesForPoi/findPoisForFrame` 포팅(server `geoMatch.ts`). 입력은 스토어 데이터.
|
||||
- [ ] `client/src/types/dom.d.ts` (또는 vite-env.d.ts) — `HTMLInputElement` `webkitdirectory`/`directory` 보강.
|
||||
- [ ] `client/src/components/player/VideoPlayer.tsx` — "폴더 선택" UI 추가 + `onSelectFolder` → `loadFolderGeoData` + `loadLocalFile`.
|
||||
- [ ] `client/src/components/geo/GeoSearch.tsx` — fetch 3곳 제거 → 스토어/포팅 함수.
|
||||
- [ ] `client/src/components/geo/StationVerify.tsx` — fetch 3곳 제거 → 스토어/포팅 함수.
|
||||
- [ ] `client/src/components/overlay/StationOverlay.tsx` — fetch 3곳 제거 → 스토어 구독, origin 스토어화.
|
||||
- [ ] `client/src/components/overlay/RoutePanel.tsx` — fetch 2곳 제거 → 스토어.
|
||||
- [ ] (정리) 컴포넌트 내 중복 `stationOrder`/`GeoPoint` 선언을 공용 타입/유틸로 통합.
|
||||
- [ ] `npm run build -w client` 통과 확인(tsc + vite).
|
||||
- [ ] 수동 검증: `samplevideo/` 폴더 선택 → 영상 재생 + 측점/POI/중심선 오버레이 + 측점 패널 목록 표시.
|
||||
- [ ] `회덕` 등 영상명 하드코딩이 클라이언트 경로에 없음을 확인(`grep -r 회덕 client/src` → 0건; RoutePanel 주석의 '대전조차장/회덕'은 단순 주석이므로 무관).
|
||||
|
||||
---
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- `npm run build -w client` (= `tsc && vite build`)가 **타입 에러 없이** 통과.
|
||||
- "폴더 선택"으로 `samplevideo/`(또는 동일 구조 폴더) 선택 시:
|
||||
- 폴더 내 영상이 재생된다(`createObjectURL`).
|
||||
- 측점/POI/선로 중심선 오버레이가 표시된다(`StationOverlay`).
|
||||
- 우측 "측점" 탭에 측점 목록이 채워지고, 클릭 시 해당 프레임으로 이동한다.
|
||||
- "지리정보" 탭 검색/역조회가 클라이언트 데이터로 동작한다.
|
||||
- RoutePanel 미니맵이 시점/종점/현재 km를 표시한다.
|
||||
- 동작이 **영상명 비의존적**: `'회덕'` 등 하드코딩 없음. 다른 base명 폴더도 동일 구조면 동작.
|
||||
- EUC-KR 한글(측점명/POI 카테고리)이 깨지지 않고 표시된다(`TextDecoder('euc-kr')`).
|
||||
|
||||
---
|
||||
|
||||
## 토큰 사용 기록
|
||||
|
||||
> 추정치(k = 1,000 tokens). 각 에이전트 자체 보고 입력/출력 추정 + 측정된 subagent 총 토큰(측정 합계) 기준.
|
||||
|
||||
| 단계 | 에이전트 | 입력(추정) | 출력(추정) | 측정 합계 |
|
||||
|------|----------|-----------|-----------|----------|
|
||||
| ①설계 | DESIGN/DOCUMENTATION | ~46k | ~9k | 98.7k |
|
||||
| ②a 구현·기반 | DEV-FOUNDATION (types/store/utils + 폴더선택 UI) | ~60k | ~9k | 99.3k |
|
||||
| ②b 구현·소비 | DEV-CONSUMER: GeoSearch | ~30k | ~2k | 52.0k |
|
||||
| ②b 구현·소비 | DEV-CONSUMER: StationVerify | ~24k | ~2k | 48.1k |
|
||||
| ②b 구현·소비 | DEV-CONSUMER: StationOverlay | ~32k | ~3k | 66.8k |
|
||||
| ②b 구현·소비 | DEV-CONSUMER: RoutePanel | ~30k | ~3k | 55.8k |
|
||||
| ③검증 | REVIEW/검수 | ~62k | ~3k | 105.1k |
|
||||
| — | (기반 1차 시도, 529 과부하로 무산) | 0 | 0 | 0 |
|
||||
| **합계** | **7개 에이전트** | **~284k** | **~31k** | **≈526k** |
|
||||
|
||||
> ②b 4개 소비 에이전트는 **병렬 실행**(서로 다른 파일)으로 벽시계 시간 단축. 판정: **ship** (빌드 통과, blocker 0, 발견 이슈 전부 nit).
|
||||
|
||||
---
|
||||
|
||||
## 후속 수정 (2026-06-17)
|
||||
|
||||
### 🔴 누락 컴포넌트: StationBar (측점 바) — 사용자 보고 "측점 패널 안 보임"
|
||||
- **원인**: 초기 탐색이 geo 소비 컴포넌트로 GeoSearch/StationVerify/StationOverlay/RoutePanel 4개만 식별. 영상 하단의 측점 바 [client/src/stationbar/StationBar.tsx](../client/src/stationbar/StationBar.tsx)는 누락되어, 여전히 `/api/geo/pois`·`/api/geo/frames`를 직접 fetch. → 폴더 선택으로 채운 클라이언트 데이터를 못 읽고, 서버 미기동 시 측점이 표시되지 않음.
|
||||
- **수정**: StationBar를 다른 컴포넌트와 동일하게 geoStore로 연결.
|
||||
- `pois` = `[...stations, ...pois]` (store 파생), `frames` = `storeFrames`로 교체. 두 `fetch` useEffect 제거.
|
||||
- precompute/세그먼트 effect 의존성 `framesVersion` → `storeFrames`. 데이터 비었을 때 `setReady(false)`로 idle 복귀.
|
||||
- `npm run build -w client` 통과 확인. 클라이언트 전체에 live `/api/geo` fetch 0건.
|
||||
- **교훈**: 다음 탐색 시 `grep -rn "fetch(.*\/api\/geo"`로 **소비처 전수 조사**를 먼저 수행해 누락 방지.
|
||||
</content>
|
||||
</invoke>
|
||||
64
agent-docs/feature-stationbar-folder-driven.md
Normal file
64
agent-docs/feature-stationbar-folder-driven.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 기능: StationBar 완전 폴더-구동화 (하드코딩/Mock 제거)
|
||||
|
||||
## 목표
|
||||
StationBar(영상 하단 측점 바)가 표시하는 **모든 데이터를 선택한 폴더의 파일에서** 가져오게 한다. 코드에 노선 데이터를 하드코딩하지 않는다. 폴더 CSV에 없는 정보(노선명·방향·시종점 역명·구조물)는 폴더 내 보조 파일 `<base>.route.json`(없으면 `route.json`)에서 읽는다.
|
||||
|
||||
## 사용자 보고 / 현상
|
||||
- 빈 화면에서도 mock 노선("신탄진"~"대전", `158k700`)이 보였음 → StationBar 폴더 로드 전 숨김으로 1차 처리 완료.
|
||||
- 잔여: 폴더 로드 후에도 터미널명·이정(mileage) 배지가 **mock**(ROUTE_LEGS 픽셀 앵커, Timeline 하드코딩)에서 나옴.
|
||||
|
||||
## 현재 소스 (감사 결과 요약)
|
||||
| 표시 요소 | 현재 소스 | 분류 |
|
||||
|----------|----------|------|
|
||||
| 트랙 px 경계/스케일/transform | mocks/route.ts | LAYOUT(유지) |
|
||||
| 방향 구간색·이정 라벨(kmLabels) | geoStore frames+stations 계산 | 이미 폴더-구동 |
|
||||
| 커서 이정 배지 텍스트 | `formatMileage(mileageAtPx(cursorPx))` = mock ROUTE_LEGS | ROUTE DATA → 폴더 |
|
||||
| 터미널명 신탄진/대전 | Timeline.tsx 하드코딩 | ROUTE DATA → route.json |
|
||||
| 노선명/방향/길이/시간 | mocks/routeInfo.ts (대부분 미사용) | ROUTE DATA → route.json |
|
||||
| 구조물(교량/터널) | mocks/segments.ts(미사용) / POI category | ROUTE DATA → route.json + POI |
|
||||
|
||||
> mocks/{routeInfo,segments,timeline,mileage}.ts 의 다수 export는 현재 **미사용(dead)**. route.ts 의 ROUTE_LEGS만 mileage.ts 경유로 커서 배지에 실사용.
|
||||
|
||||
## 보조 파일 `route.json` (이미 생성: samplevideo/하행)회덕-대전조차장.route.json)
|
||||
```json
|
||||
{
|
||||
"routeInfo": { "name","direction","lengthKm","durationSec","startStationName","endStationName" },
|
||||
"structures": [ { "id","type":"bridge|tunnel","name","startMileage","endMileage" } ]
|
||||
}
|
||||
```
|
||||
- 위치: 영상과 같은 폴더 루트. 파일명 `<base>.route.json` 우선, 없으면 `route.json`. UTF-8.
|
||||
- 모든 필드 optional. 없으면 폴더 CSV에서 유도(터미널명=첫/끝 측점 title) 또는 미표시.
|
||||
|
||||
## 변경 설계 (To-Be)
|
||||
1. **types/geo.ts**: `RouteMeta`(routeInfo + structures) 추가, `FolderGeoData`·store shape에 `routeMeta: RouteMeta | null`.
|
||||
2. **utils/geoData.ts**: `parseRouteMeta(files)` — `<base>.route.json`/`route.json` 탐색 → `await file.text()` → `JSON.parse`. `loadFolderGeoData` 반환에 `routeMeta` 추가.
|
||||
3. **store/geoStore.ts**: `routeMeta` 상태 + `loadFromFolder`에서 set, `clear()`에서 null.
|
||||
4. **stationbar/StationBar.tsx**:
|
||||
- 커서 이정 배지: mock `mileageAtPx(cursorPx)` 대신 **폴더 데이터 기반 실제 km**(viewedRef의 km = 드론GPS 최근접 측점, 이미 계산됨) 사용.
|
||||
- 구조물 마커: `routeMeta.structures`(mileage→최근접 프레임 시간→px)로 생성. (POI category 보조는 유지 가능)
|
||||
- Timeline에 `startStationName/endStationName` 전달: `routeMeta.routeInfo` 우선, 없으면 첫/끝 측점 title.
|
||||
5. **stationbar/components/Timeline/Timeline.tsx**: 하드코딩 "신탄진"/"대전" 제거 → props로 수신(없으면 빈 문자열/미표시).
|
||||
6. **mocks**: 미사용 dead export는 그대로 두되(삭제 안 함), 실사용 ROUTE_LEGS 의존(커서 배지)은 4번으로 제거. LAYOUT 기하 상수는 UI 레이아웃이므로 유지.
|
||||
|
||||
## 완료 기준
|
||||
- `npm run build -w client` 통과(타입 에러 0).
|
||||
- 폴더 선택 시: 커서 배지 이정·터미널명·구조물이 **route.json + 폴더 CSV**에서 나온다. mock ROUTE_LEGS/하드코딩 신탄진·대전 미사용.
|
||||
- route.json 없는 폴더: 크래시 없이 측점 title 기반 폴백(터미널명=첫/끝 측점) 또는 미표시.
|
||||
- 영상명/노선 비의존.
|
||||
|
||||
## 토큰 사용 기록
|
||||
> 추정치(k=1,000). 측정 합계 = subagent 총 토큰.
|
||||
|
||||
| 단계 | 에이전트 | 입력(추정) | 출력(추정) | 측정 합계 |
|
||||
|------|----------|-----------|-----------|----------|
|
||||
| ①조사 | Explore 감사(StationBar mock 매핑) | ~55k | ~7k | ~80k |
|
||||
| ①설계 | 본 문서 + route.json 작성 | 오케스트레이터 | — | — |
|
||||
| ②구현 | DEV (geoData/geoStore/types + StationBar/Timeline) | ~50k | ~5k | 55.5k |
|
||||
| ③검수 | 오케스트레이터 직접(빌드+grep 검증) | — | — | — |
|
||||
|
||||
## 구현 결과 (완료)
|
||||
- ✅ `route.json` 보조 파일 로딩(`<base>.route.json`→`route.json`), geoStore `routeMeta`.
|
||||
- ✅ 커서 이정 배지: 폴더 데이터(viewedRef.km, 드론 GPS 최근접 측점) 기반. mock `mileageAtPx`(ROUTE_LEGS) 런타임 의존 제거(주석만 잔존).
|
||||
- ✅ 터미널명: route.json `routeInfo` → 폴백 첫/끝 측점 title. Timeline 하드코딩 신탄진/대전 제거(props 전환).
|
||||
- ✅ 구조물: route.json `structures`(mileage→시간→px), 없으면 POI category 폴백.
|
||||
- ✅ `npm run build -w client` 통과. mock 파일/dead export는 보존(삭제 안 함), LAYOUT 기하 상수 유지.
|
||||
Reference in New Issue
Block a user