GhiVideo 소스 복제 — v4 작업 시작 기준
기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# DefVideo → GhiVideo 업그레이드 상세 문서
|
||||
|
||||
> 작성일: 2026-06-30
|
||||
> 비교 기준: `b23042/DefVideo`(구버전) → `b23042/GhiVideo`(신버전)
|
||||
> 두 저장소는 git 히스토리가 분리된 별도 저장소이며, 동일 코드베이스에서 파생되었다. 본 문서는 실제 소스 트리(`diff -r`)를 직접 비교하여 작성되었다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보기
|
||||
|
||||
| 영역 | 변경 규모 | 평가 |
|
||||
|------|----------|------|
|
||||
| **서버 (server)** | `app.ts` +2줄, 신규 `elevation.ts`(69줄) | 거의 동일 — 골격 유지 |
|
||||
| **공유 타입 (shared)** | 변경 없음 | 동일 |
|
||||
| **클라이언트 (client)** | 약 **+3,000줄** 순증 | **대폭 발전** |
|
||||
|
||||
**핵심 결론**: 기반 골격(서버 스트리밍/HLS/업로드, 공유 타입)은 그대로 유지하면서, **측점(체이니지) 기반 주행영상 분석 도메인 기능**이 본격적으로 구현된 한 단계 발전한 버전이다. 세 갈래로 요약하면:
|
||||
|
||||
1. **V2.0 데이터 형식 대응** — 영상 옆 KMZ/KML을 1순위로 POI·구조물 추출(없으면 `building/` 5종 CSV 폴백), 측점 CSV 기반 중심선 생성, 자동 인코딩 감지, 방향전환점 추출
|
||||
2. **투영 정확도 개선** — 지오이드 보정, 지면고도(DEM) 적용, 역투영 기반 위치 보정
|
||||
3. **영상 오버레이/편집 UI 고도화** — 라벨 평활, 겹침 억제, 컴팩트 팝업, 드래그 편집, 나침반 미니맵
|
||||
|
||||
---
|
||||
|
||||
## 1. 파일 단위 변경 요약
|
||||
|
||||
### 변경된 기존 파일
|
||||
|
||||
| 파일 | 변경량 | 내용 |
|
||||
|------|--------|------|
|
||||
| `client/src/components/overlay/StationOverlay.tsx` | **+1,188 / −167** | 라벨 평활·겹침억제·구조물표출·컴팩트팝업·편집모드 (최대 변경) |
|
||||
| `client/src/utils/geoData.ts` | **+660 / −111** | V2.0 폴더 파싱·구조물·방향전환점·POI보정 |
|
||||
| `client/src/stationbar/StationBar.tsx` | +340 / −87 | 이동거리축·시설등급필터·종점역 미도착 |
|
||||
| `client/src/stationbar/components/Timeline/Timeline.tsx` | +248 / −27 | 구조물 겹침필터·라벨 그룹핑 |
|
||||
| `client/src/components/player/VideoPlayer.tsx` | +168 / −40 | 영상컨트롤 UI 통합·smoothTimeRef·폴더 드롭 |
|
||||
| `client/src/utils/geoProjection.ts` | +138 / −5 | 지오이드 보정·역투영 함수 3종 |
|
||||
| `client/src/types/geo.ts` | +54 / −7 | 구조물/방향전환점/보정 타입 |
|
||||
| `client/src/store/geoStore.ts` | +47 / −3 | POI 보정 분리관리·구조물 상태 |
|
||||
| `client/src/components/overlay/RoutePanel.tsx` | +27 / −5 | 구조물 필터·방향 우선순위 |
|
||||
| `client/src/store/playerStore.ts` | +13 / −1 | videoReady·영상 해상도 상태 |
|
||||
| `client/src/hooks/useVideoPlayer.ts` | +8 / −0 | 영상 로드 이벤트 추적 |
|
||||
| `server/src/app.ts` | +2 / −0 | elevation 라우트 등록 |
|
||||
|
||||
### 신규 추가 파일 (DefVideo엔 없음)
|
||||
|
||||
| 파일 | 줄수 | 역할 |
|
||||
|------|------|------|
|
||||
| `client/src/components/overlay/Minimap.tsx` | 69 | 드론 방위각(heading-up) 나침반 미니맵 |
|
||||
| `client/src/store/settingsStore.ts` | 90 | 사용자 표시 설정 중앙 관리(localStorage 영속) |
|
||||
| `client/src/utils/chainage.ts` | 64 | 측점값(체이니지) 계산·중심선 투영 |
|
||||
| `server/src/routes/elevation.ts` | 69 | DEM(표고) 데이터 프록시 API |
|
||||
|
||||
---
|
||||
|
||||
## 2. 데이터 처리 — V2.0 형식 대응 (`geoData.ts`)
|
||||
|
||||
가장 많이 변경된 유틸. 구버전은 `center.csv` + 단일 POI CSV 중심이었으나, 신버전은 **영상 옆(root)의 KMZ/KML을 1순위 소스**로 POI·구조물을 직접 추출하고, `building/` 하위 CSV는 **KMZ가 없을 때의 폴백**으로 둔다(`loadFolderGeoData`의 `useKmz` 분기). 측점만 KMZ에 없어 항상 CSV에서 읽되, 측점·역사 CSV는 `building/` 또는 root 어디서든 찾는다(폴더 삭제 대비). 즉 현재 데이터는 KMZ(root) + 측점 CSV(root)만으로 동작하며 building 폴더가 필수는 아니다.
|
||||
|
||||
### 2.1 자동 인코딩 감지
|
||||
- **구버전**: 파일별로 `utf-8` / `euc-kr`를 코드에 하드코딩
|
||||
- **신버전**: ArrayBuffer 앞 3바이트(BOM `EF BB BF`)를 검사해 UTF-8/EUC-KR 자동 선택 → 혼재된 인코딩 자동 처리
|
||||
|
||||
### 2.2 데이터 소스 우선순위 (KMZ 우선 · CSV 폴백)
|
||||
```
|
||||
1순위: 영상 옆(root) *.kml / *.kmz → parseKmz → POI·구조물 직접 추출
|
||||
(KMZ에 POI/구조물 있으면 useKmz=true → 아래 building CSV 미사용)
|
||||
|
||||
측점: 01)측점.csv (building/ 또는 root) → 측점 + 방향전환점 (KMZ에 없어 항상 CSV)
|
||||
역사: 02)지장물_역사.csv (building/ 또는 root) → 스테이션바 전용
|
||||
|
||||
폴백(KMZ 없을 때만):
|
||||
building/02)지장물.csv → POI(지장물)
|
||||
building/03)교량.csv → 구조물(bridge)
|
||||
building/04)터널.csv → 구조물(tunnel)
|
||||
building/05)출입문번호.csv → POI(출입문)
|
||||
building/06)구교.csv → 구조물(bridge, 구교 아이콘)
|
||||
```
|
||||
- `center.csv`를 더 이상 찾지 않고, **측점(01)을 측점값 순으로 이어 중심선을 생성**(`buildCenterlineFromStations()`)
|
||||
- 지장물·교량·터널·구교·출입문 CSV는 `building/` 안에서만 찾지만, KMZ가 있으면 통째로 무시되므로 KMZ 기반 데이터에서는 building 폴더가 없어도 정상 동작한다.
|
||||
|
||||
### 2.3 CSV 파싱 견고성 강화
|
||||
- `makeFieldIndexer()`: 헤더명 기반 인덱싱 실패 시 위치 인덱스로 폴백 → 깨진 EUC-KR 헤더도 안전
|
||||
- `cell()`: 음수/범위 초과 인덱스에서 빈 문자열 반환 → 크래시 방지
|
||||
- `rowProps()`: CSV 원본 속성을 `{k, v}[]` 쌍으로 추출 → UI 라벨 클릭 팝업에 그대로 표출
|
||||
|
||||
### 2.4 신규 파싱 항목
|
||||
- **측점 + 방향전환점**(`parseStations()`): `Z좌표_한국`(정표고, EPSG:5186) 우선 사용, 비고 컬럼에서 `방향전환점(상행->하행, 02:05)` 정규식 추출 → `DirectionChange` 생성
|
||||
- **구조물**(`parseStructures()`): 교량/터널/구교 3종 통합, 위경도 대한민국 범위(33–39°N, 124–132°E) 검증, `연장(m)·시설종별·분류` 채움
|
||||
- **역사**: `type='station'`으로 변환 → 스테이션바에만 표시(영상 오버레이 제외)
|
||||
- **KMZ 원본**(`parseKmz()`): 구글 어스 KMZ가 있으면 직접 추출(CSV 중복 회피), 측점만 항상 CSV 사용
|
||||
- **POI 위치 보정**(`parsePoiOverrides()`/`applyPoiOverrides()`): 드래그 보정값을 `<base>_poi_overrides.json`(`{title: {lat,lon,z}}`)으로 저장/복원
|
||||
|
||||
---
|
||||
|
||||
## 3. 투영 정확도 개선 (`geoProjection.ts`, `chainage.ts`)
|
||||
|
||||
### 3.1 카메라 파라미터 신규 필드
|
||||
- `geoidOffset`(m): 정표고(EL) → 타원체고 변환. 드론 `abs_alt`(타원체고)와 datum 일치 (대전 기본 25.8m, KNGeoid18)
|
||||
- `poiZOffset`(m): POI 전용 표고 보정. 선로 지면 가정 오차를 패널에서 조절
|
||||
|
||||
### 3.2 카메라 좌표 신규 필드
|
||||
- `distH`: 수평 거리(m) — POI 거리필터용
|
||||
- `fwd`/`side`: 진행방향(yaw) 기준 앞쪽/옆쪽 거리(m) → **비등방 거리필터**(앞은 멀리, 옆은 가깝게) 구현 가능
|
||||
|
||||
### 3.3 역투영 함수 3종 신규 (편집 기능의 수학적 기반)
|
||||
- `worldFromPixel(px, py, range)`: 화면 픽셀 + 슬랜트거리 → 월드좌표(lat/lon/z). POI 드래그 보정(수평+수직 동시)
|
||||
- `solveZForPixelY(...)`: 특정 화면 세로위치에 맞는 표고(z) 역산. "앞으로 밀려 보이는 슬라이드" 보정
|
||||
- `groundPointFromPixel(px, py, zGround)`: 화면점과 특정 고도평면의 교차점(lat/lon). 지면 좌표 보정
|
||||
|
||||
### 3.4 측점/체이니지 계산 (`chainage.ts`, 신규)
|
||||
- `kmFromTitle("157K970")` → `157970`(m), `fmtKm10()` → `"157k970"`
|
||||
- `buildChainLine()`: 측점 → 평면투영 폴리라인
|
||||
- `projectToChain()`: 드론 GPS → 폴리라인 투영 → `{km(측점값), offsetM(선로 수직이격)}`
|
||||
- `nearestChainPoint()`: 최근접 측점 검색 → StationBar·VideoPlayer HUD 공유
|
||||
|
||||
---
|
||||
|
||||
## 4. 영상 오버레이 고도화 (`StationOverlay.tsx`)
|
||||
|
||||
### 4.1 라벨 평활 & 이상치 거부
|
||||
- **이상치 거부**: 한 프레임에 화면폭 12%(`REJECT_DIST`) 이상 점프하면 노이즈로 보고 이전 위치 유지. 단 연속 8프레임(`MAX_REJECT_FRAMES`) 초과 시 수용 → 시크/재등장에서 멈추지 않음
|
||||
- **속도 적응형 평활(One Euro 방식)**: 떨림(방향 왕복)은 강하게 평활, 실제 이동(방향 일관)은 즉시 추종 → 1배속 떨림 제거 + 빠른 배속 지연 해소
|
||||
- 조절 파라미터: `smoothMinAlpha`(정지 시 최소 추종), `smoothSpeedRef`(즉시추종 기준속도)
|
||||
|
||||
### 4.2 POI 겹침 억제
|
||||
- 화면상 가로 10%·세로 3.5% 이내 마커는 겹침으로 판정 → 드론에 더 가까운 것만 표시
|
||||
- 같은 건물 여러 업체/시설 중 주요한 것만 노출 → 화면 정리
|
||||
|
||||
### 4.3 구조물 표출 & 컴팩트 팝업
|
||||
- 교량/터널/구교를 POI처럼 라벨로 표출(이모지 🌉 구교, 🚪 출입문 추가)
|
||||
- **컴팩트 필드**: 라벨 옆 항상 표시(시설종별 → 구조형식 → 연장 → 폭 → 용도 → 준공연도 순)
|
||||
- 클릭 시 전체 팝업 확장, 다시 클릭 시 컴팩트로 토글. **다중 팝업** 누적 가능, ESC로 정리
|
||||
|
||||
### 4.4 POI 높이 모드 & DEM 자동 적용
|
||||
- **드론 높이 기준**: POI를 드론보다 N미터 아래 고정(`droneHeightDrop`)
|
||||
- **지면고도 기준**: POI를 실제 지형고도에 고정 → 드론 오르내려도 지물에 붙음
|
||||
- 서버 elevation API(SRTM 30m DEM)로 모든 좌표의 실제 표고 일괄 적용 → 먼 곳에서 밀리는 현상 완화
|
||||
|
||||
### 4.5 드래그 편집 & 세로화각 보정
|
||||
- POI를 마우스로 드래그 → `groundPointFromPixel`로 위경도 역산
|
||||
- 표고 슬라이더로 높이 미세조정, 개별 리셋 가능
|
||||
- **세로화각 보정 모드**: 라벨이 상하로 어긋날 때 POI를 실제 위치로 드래그 → 세로 화각(sensorH)만 역산 자동보정
|
||||
- 보정값 JSON 내보내기/가져오기, 일괄 초기화
|
||||
|
||||
### 4.6 좌표 정렬 (object-fit:cover 보정)
|
||||
- `coverRef`: 영상이 `object-fit:cover`로 크롭된 영역을 매 프레임 계산 → 정규좌표(0–1) ↔ 화면 px 정확 변환 → 포인터 히트테스트 정확
|
||||
|
||||
---
|
||||
|
||||
## 5. 나침반 미니맵 (`Minimap.tsx`, 신규)
|
||||
|
||||
- 우측 상단 고정 150×150px 아날로그 나침반 위젯(heading-up)
|
||||
- 고정 프레임(배경원/외곽링) + 회전 카드(6° 간격 60눈금 + N 문자 + 빨강/흰 삼각형)
|
||||
- 부모의 `--rot` CSS 변수로 부드럽게 회전, StationOverlay RAF에서 매 프레임 갱신(360° 언랩)
|
||||
- 효과: 영상만으로 드론이 향하는 방향을 한눈에 파악
|
||||
|
||||
---
|
||||
|
||||
## 6. 하단 측점바 & 타임라인 (`StationBar.tsx`, `Timeline.tsx`)
|
||||
|
||||
### 6.1 측점 정확도 개선
|
||||
- **이동거리축 추가**: 드론 실제 GPS 이동량을 시간축에 반영 → 호버(공중 대기) 구간 가시화, 전진/후진 색 리본(주황/하늘색) 구분
|
||||
- **영상 FPS 자동 적응**: 고정 29.97 대신 `마지막 프레임 / 재생시간`으로 영상별 계산 → 측점 배지 정확도 향상
|
||||
- **kmExists 플래그**: 실제 측점값 존재 여부 구분 → 좌표 반경만으로 잡힌 통과는 라벨 제외
|
||||
|
||||
### 6.2 시설등급 필터 & 구조물 표시
|
||||
- 1종/2종/3종/기타 선택적 표시(settingsStore 연동, 체크박스 즉시 반영)
|
||||
- 긴 이름 2줄 분할(CJK 1.0/영문 0.55 가중치)
|
||||
- 상/하행 평행 변형은 영상 진행방향에 따라 하나만 표시
|
||||
- 동명 구조물 반복 통과 → 중앙 라벨 1개 + 통과점 점선 드롭 + 수평 브래킷
|
||||
|
||||
### 6.3 종점역 미도착 시각화
|
||||
- 영상이 종점역에 미도달 시 트랙 우측을 회색(재생불가)으로 표시(상한 15%)
|
||||
- 종점역 마커를 '속 빈 링' 스타일(미도착)로
|
||||
|
||||
---
|
||||
|
||||
## 7. 영상 플레이어 (`VideoPlayer.tsx`, `useVideoPlayer.ts`, `playerStore.ts`)
|
||||
|
||||
- **영상제어 UI 수평 통합**: 좌하단 컨트롤을 한 줄 가로 배열(`영상제어 | 배속 | 프레임 | 좌측패널 | 선형 | 드론궤적 | GPS/고도 | 시설등급`), flex-wrap 자동 줄바꿈
|
||||
- **smoothTimeRef**: 벽시계 기준 단조 보간 시간 → StationBar가 ref로 직접 읽어 React 리렌더 없이 transform 갱신 → 60fps 부드러운 커서, 시크 시 즉시 재동기화
|
||||
- **videoReady 게이트**: `loadeddata` 이후에만 오버레이 렌더 → 영상보다 먼저 그려지는 깜빡임 제거
|
||||
- **영상 해상도 추적**: `videoWidth/videoHeight`로 object-fit:cover 정렬 정확화
|
||||
- **폴더 드롭 지원**: `webkitGetAsEntry()` 디렉토리 재귀 순회 → 영상+측점/POI 폴더 통째 로드
|
||||
- **드론 정보 HUD**: 현재 프레임 최근접 드론 GPS/고도 항상 표시
|
||||
- **버튼 정렬**: `min-w-[96px]` 고정폭 + amber 활성색, 프레임번호 6자리 고정폭
|
||||
|
||||
---
|
||||
|
||||
## 8. 설정 중앙 관리 (`settingsStore.ts`, 신규)
|
||||
|
||||
Zustand + persist(`localStorage: 'ghivideo.settings'`)로 다음 표시 설정을 영속화한다.
|
||||
|
||||
| 키 | 기본값 | 용도 |
|
||||
|----|--------|------|
|
||||
| `gradeFilter` | `{1종:T, 2종:T, 3종:F, 기타:T}` | 시설종별 표시 필터 |
|
||||
| `poiOverlapExclude` | `true` | 겹친 POI 숨김 |
|
||||
| `showRoutePanel` | `true` | 좌측 노선 패널 |
|
||||
| `showStationDiag` | `false` | 측점 진단 HUD |
|
||||
| `showCenterline` | `true` | 선형(중심선) 오버레이 |
|
||||
| `showDronePath` | `true` | 드론 궤적 오버레이 |
|
||||
|
||||
- 누락된 키는 기본값으로 자동 보충(스키마 진화 대비)
|
||||
- `isGradeVisible()` 공유 함수로 StationBar·RoutePanel·StationOverlay가 동일 규칙 적용
|
||||
|
||||
---
|
||||
|
||||
## 9. 서버: 고도 API 프록시 (`elevation.ts`, 신규)
|
||||
|
||||
- 브라우저 COEP/CSP 제약으로 클라이언트가 외부 DEM API를 직접 호출 불가 → **서버 중계**
|
||||
- `GET /api/elevation?lat=36.4,36.5&lon=127.4,127.5` (배치, 콤마 구분)
|
||||
- 응답: `{ elevation: number[], source }`
|
||||
- 폴백 전략: ① opentopodata SRTM 30m(정밀) → ② open-meteo 90m(폴백)
|
||||
- `app.ts`에 `app.use('/api/elevation', elevationRouter)` 등록 (서버 변경은 이것 + 라우트 파일이 전부)
|
||||
|
||||
---
|
||||
|
||||
## 10. 타입/스토어 변경 요약
|
||||
|
||||
### `geo.ts` 신규/확장
|
||||
- `GeoPoint.props`: CSV 원본 속성 `{k,v}[]` (팝업 표시)
|
||||
- `RouteStructure`: `lengthM`(연장) · `category`(교량/터널/구교/역사) · `grade`(시설종별) · `props` 추가
|
||||
- `DirectionChange`(신규): `station` · `from`/`to` · `atSeconds`
|
||||
- `PoiOverride` / `PoiOverrideMap`(신규): 드래그 보정값
|
||||
- `RouteMeta.endStationGapMeters`: 종점역 미도착 거리
|
||||
- `FolderGeoData`: `structures` · `directionChanges` · `poiOverrides` 추가
|
||||
|
||||
### `geoStore.ts`
|
||||
- POI 보정 분리관리: `basePois`(원본) + `poiOverrides`(보정맵) → `applyPoiOverrides()` 결과가 `pois`
|
||||
- 신규 상태: `structures`, `directionChanges`
|
||||
- 신규 메서드: `setPoiOverride()`, `clearPoiOverride()`, `setPoiOverrides()`
|
||||
- `loadFromFolder()`: Promise.all 병렬 로딩으로 8개 파서 동시 실행
|
||||
|
||||
---
|
||||
|
||||
## 11. 사용자 체감 개선 총괄
|
||||
|
||||
| 항목 | 개선 효과 |
|
||||
|------|----------|
|
||||
| **라벨 안정성** | 떨림/튐 제거 — 이상치 거부 + 속도적응 평활 |
|
||||
| **화면 가독성** | 겹친 POI/구조물 자동 숨김 |
|
||||
| **정보 탐색** | 다중 팝업 + 컴팩트/전체 토글 + ESC 정리 |
|
||||
| **위치 정확도** | 지오이드 보정 + DEM 실제 표고 + 역투영 편집 |
|
||||
| **편집 편의** | 드래그 + 표고 슬라이더 + 세로화각 자동보정 + JSON 저장 |
|
||||
| **측점 정확도** | 이동거리축(호버 가시화) + 영상별 FPS 자동 |
|
||||
| **커서 부드러움** | smoothTimeRef 단조보간 → 60fps |
|
||||
| **작업 효율** | 폴더 통째 드롭 로드, 설정 localStorage 영속 |
|
||||
| **방향 인지** | 나침반 미니맵으로 드론 방위각 실시간 표시 |
|
||||
| **오버레이 안정** | videoReady 게이트로 깜빡임 제거 |
|
||||
|
||||
---
|
||||
|
||||
*본 문서는 `DefVideo`와 `GhiVideo`의 소스 트리 전체 비교(`diff -r`) 및 주요 변경 파일의 라인 단위 분석을 통해 작성되었다.*
|
||||
@@ -0,0 +1,55 @@
|
||||
/* 한맥 기술개발센터 회의자료(HWP) 양식 모사 — md→html→pdf 공용 */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 22mm 18mm 18mm 18mm;
|
||||
@top-left { content: "한맥 기술개발센터"; font-family: "Malgun Gothic"; font-size: 8.5pt; color: #555; }
|
||||
@top-right { content: "대외비 / 회의자료"; font-family: "Malgun Gothic"; font-size: 8.5pt; color: #a33; }
|
||||
@bottom-center { content: "기술로 사람과 자연이 함께하는 세상을 만들어 갑니다."; font-family: "Malgun Gothic"; font-size: 8.5pt; color: #999; }
|
||||
@bottom-right { content: counter(page); font-family: "Malgun Gothic"; font-size: 8.5pt; color: #999; }
|
||||
}
|
||||
|
||||
html { font-size: 10.5pt; }
|
||||
body {
|
||||
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||
color: #1a1a1a; line-height: 1.55; margin: 0 auto; max-width: 900px; padding: 8px 10px;
|
||||
}
|
||||
|
||||
/* 제목 */
|
||||
h1 {
|
||||
text-align: center; font-size: 17pt; font-weight: 800; color: #111;
|
||||
margin: 2px 0 4px; padding-bottom: 8px; border-bottom: 2.5px solid #333;
|
||||
}
|
||||
.subhead { text-align: center; color: #666; font-size: 9.5pt; margin: 0 0 18px; }
|
||||
|
||||
/* □ 절 제목 (## 으로 작성, 텍스트에 □ 포함) */
|
||||
h2 {
|
||||
font-size: 13pt; font-weight: 800; color: #14305e;
|
||||
margin: 18px 0 8px; padding: 0; border: none;
|
||||
}
|
||||
/* ▷ 소제목 (### 으로 작성) */
|
||||
h3 { font-size: 11pt; font-weight: 700; color: #333; margin: 12px 0 5px; }
|
||||
|
||||
ul { margin: 4px 0 8px; padding-left: 18px; }
|
||||
li { margin: 2px 0; }
|
||||
strong { color: #111; }
|
||||
a { color: #14305e; text-decoration: none; }
|
||||
|
||||
/* 표 — HWP 느낌(전 테두리, 헤더 회색) */
|
||||
table { border-collapse: collapse; width: 100%; margin: 8px 0 12px; font-size: 9.5pt; }
|
||||
th, td { border: 1px solid #555; padding: 5px 8px; vertical-align: top; text-align: left; }
|
||||
th { background: #e9edf3; color: #14305e; font-weight: 700; text-align: center; }
|
||||
td.c, th.c { text-align: center; }
|
||||
|
||||
/* ☞ 제언 박스 */
|
||||
blockquote {
|
||||
border: 1px solid #c9b27a; background: #fffbf0; border-radius: 4px;
|
||||
margin: 10px 0; padding: 8px 12px; color: #5a4626; font-size: 9.8pt;
|
||||
}
|
||||
|
||||
figure { margin: 10px 0; text-align: center; page-break-inside: avoid; break-inside: avoid; }
|
||||
figure svg { width: 100%; max-width: 720px; height: auto; }
|
||||
figure figcaption { font-size: 8.8pt; color: #777; margin-top: 4px; }
|
||||
text { font-family: "Malgun Gothic", sans-serif; }
|
||||
|
||||
h1, h2, h3 { break-after: avoid; }
|
||||
table, figure { break-inside: avoid; }
|
||||
@@ -0,0 +1,77 @@
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 18mm 16mm 16mm 16mm;
|
||||
@bottom-center {
|
||||
content: counter(page) " / " counter(pages);
|
||||
font-family: "Malgun Gothic", sans-serif;
|
||||
font-size: 9pt;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
html { font-size: 11pt; }
|
||||
|
||||
body {
|
||||
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||
color: #23272e;
|
||||
line-height: 1.65;
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.7rem;
|
||||
color: #b45309; /* amber-700 — 앱 활성색 계열 */
|
||||
border-bottom: 3px solid #f59e0b; /* amber-500 */
|
||||
padding-bottom: 8px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
color: #b45309;
|
||||
border-bottom: 1px solid #e5d3b3;
|
||||
padding-bottom: 5px;
|
||||
margin-top: 1.6em;
|
||||
}
|
||||
|
||||
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||
|
||||
a { color: #b45309; }
|
||||
|
||||
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||
|
||||
ul { padding-left: 1.25em; }
|
||||
li { margin: 0.18em 0; }
|
||||
|
||||
strong { color: #1f2937; }
|
||||
|
||||
code {
|
||||
font-family: "D2Coding", Consolas, monospace;
|
||||
background: #f4f1ea;
|
||||
border: 1px solid #e7e0d2;
|
||||
border-radius: 3px;
|
||||
padding: 0.5px 5px;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.8em 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||
th { background: #fdf3df; color: #7c2d12; }
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #f59e0b;
|
||||
margin: 0.8em 0;
|
||||
padding: 0.2em 0 0.2em 14px;
|
||||
color: #555;
|
||||
background: #fffbf2;
|
||||
}
|
||||
|
||||
/* ★ 강조 항목이 페이지 경계에서 잘리지 않게 */
|
||||
h1, h2, h3 { break-after: avoid; }
|
||||
@@ -0,0 +1,37 @@
|
||||
# RoutePanel 미니맵 추가 및 개선
|
||||
|
||||
**소요 시간**: 약 3시간
|
||||
**Context 사용량**: input ~60k / output ~20k tokens
|
||||
**이슈**: 없음
|
||||
|
||||
---
|
||||
|
||||
## 작업 내용
|
||||
|
||||
### RoutePanel 컴포넌트 생성 및 VideoPlayer 통합
|
||||
|
||||
- `client/src/components/overlay/RoutePanel.tsx` 신규 생성
|
||||
- `VideoPlayer.tsx`에 통합 (`showStations` 토글 연동)
|
||||
- props: `currentTime`, `visible`, `onSeek`
|
||||
|
||||
### 기능
|
||||
|
||||
- 세로 미니맵 패널 (화면 좌측)
|
||||
- 교량/터널 POI만 필터링하여 표시 (터널: 보라, 교량: 하늘색)
|
||||
- 초록 박스: 현재 카메라에 보이는 km 범위
|
||||
- 오렌지 마커: 현재 위치, 드래그하면 해당 측점으로 seek
|
||||
- 시점/종점 역명 표시 (역사 카테고리 POI 중 km 최소/최대)
|
||||
|
||||
### 수정 이력
|
||||
|
||||
- `StationOverlay.tsx` `alt` → `altitude` 타입 오류 수정
|
||||
- `cleanTitle()`: (상)/(하) 접미어 제거
|
||||
- km 방향 여러 차례 수정 끝에 확정: 높은 km = 위, 낮은 km = 아래
|
||||
- 패널 높이 80%, 겹침 간격 9%
|
||||
- 글씨 크기 +30%, 배경 투명도 밝게
|
||||
|
||||
## 산출물
|
||||
|
||||
- `client/src/components/overlay/RoutePanel.tsx` (신규)
|
||||
- `client/src/components/player/VideoPlayer.tsx`
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
@@ -0,0 +1,32 @@
|
||||
# StationOverlay 렌더링 끊김 분석 + history 자동화 규칙 추가
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~8k / output ~3k tokens
|
||||
**이슈**: 없음
|
||||
|
||||
---
|
||||
|
||||
## 작업 내용
|
||||
|
||||
### 1. StationOverlay 렌더링 파이프라인 분석
|
||||
- `client/src/components/overlay/StationOverlay.tsx` 코드 분석
|
||||
- 렌더링 구조: RAF 루프(60fps) + renderCache useEffect(드론 프레임 변경 시)
|
||||
- 끊김 원인 파악:
|
||||
- `panelDroneFrame` 상태 변경 시 `useEffect`에서 수백~수천 개 좌표 변환(`toCameraCoords` + `pixelFromCamera`)이 메인 스레드에서 동기 실행
|
||||
- 계산이 16ms 초과 시 RAF 프레임 드롭 → 뚝뚝 끊김
|
||||
- 측정 방법 제안: `performance.now()` 로깅으로 캐시 빌드 시간 측정
|
||||
|
||||
| 항목 | 실제 |
|
||||
|------|------|
|
||||
| RAF draw 횟수 | ~60fps (항상) |
|
||||
| 캐시 재빌드 횟수 | ~30fps (드론 프레임 변경 시) |
|
||||
| 끊김 원인 | 캐시 빌드 시 메인 스레드 블로킹 |
|
||||
|
||||
### 2. history 자동화 규칙 CLAUDE.md 추가
|
||||
- 사용자 기대: 작업 완료 시 날짜+주제별 history 파일 자동 작성
|
||||
- 현재 훅은 "리마인더/가드"만 담당, 파일 생성은 Claude 몫
|
||||
- CLAUDE.md 상단에 "작업 완료 시 필수 — 히스토리 기록" 섹션 추가
|
||||
|
||||
## 산출물
|
||||
- `CLAUDE.md` — 히스토리 기록 규칙 섹션 추가
|
||||
- `docs/history/2026-04-01_StationOverlay-렌더링분석.md` (이 파일)
|
||||
@@ -0,0 +1,52 @@
|
||||
# StationOverlay 렌더링 최적화 — 텍스트 스무딩
|
||||
|
||||
**소요 시간**: 약 90분
|
||||
**Context 사용량**: input ~40k / output ~12k tokens
|
||||
**이슈**: 없음
|
||||
|
||||
---
|
||||
|
||||
## 작업 내용
|
||||
|
||||
### 문제
|
||||
- 측점/지장물 텍스트 라벨이 영상 재생 중 뚝뚝 끊겨 표시됨
|
||||
- 드론 pitch/GPS 노이즈 + 30fps 데이터 → 60fps RAF 스텝 차이
|
||||
|
||||
### 해결 과정
|
||||
|
||||
1. **원인 분석**
|
||||
- `renderCacheRef` useEffect에서 CL 500개 × toCameraCoords = 매 33ms 메인 스레드 블로킹
|
||||
- 텍스트는 31개로 작은 부분이었음, CL이 실제 병목
|
||||
- 디버그 패널 추가로 firstY 값이 5-7px 불규칙 점프 확인
|
||||
|
||||
2. **텍스트 사전 계산 Map 도입**
|
||||
- `labelMapRef: Map<frameNum, {stationLabels, poiMarkers}>` 전 프레임 precompute
|
||||
- `requestIdleCallback` 백그라운드 계산 (메인 스레드 비블로킹)
|
||||
- RAF: `Map.get(frameNum)` O(1) 조회만
|
||||
|
||||
3. **데이터 스무딩 (smoothFrame)**
|
||||
- ±N 프레임 이동 평균 (yaw는 sin/cos 평균 후 atan2)
|
||||
- UI 슬라이더로 조절 가능 (0~60fr)
|
||||
- 기본값: smooth=10
|
||||
|
||||
4. **30fps→60fps 보간 (frac interpolation)**
|
||||
- `performance.now()`로 prop 업데이트 이후 경과 시간 추정
|
||||
- 현재/다음 프레임 사이 선형 보간 → 스텝 제거
|
||||
|
||||
5. **EMA (지수 이동 평균) 표시 위치 스무딩**
|
||||
- `displayedStRef`, `displayedPoiRef`: 각 라벨별 현재 표시 위치 유지
|
||||
- RAF마다: `pos = prev + (target - prev) × α`
|
||||
- UI 슬라이더로 α 조절 (0.01~1.0), 기본값: α=0.01
|
||||
- α=0.01: 매우 부드럽지만 약간 뒤처짐
|
||||
|
||||
6. **시각 개선**
|
||||
- 글씨 크기 2배 (9px→18px, 10px→20px)
|
||||
- bold + strokeText 4px 검정 테두리 (배경 박스 제거)
|
||||
- CL 중심선 재활성화
|
||||
|
||||
### 최종 기본값
|
||||
- smoothHalf: 10 (±333ms 이동 평균)
|
||||
- emaAlpha: 0.01 (~1600ms lag, 매우 부드러움)
|
||||
|
||||
## 산출물
|
||||
- `client/src/components/overlay/StationOverlay.tsx` 전면 개편
|
||||
@@ -0,0 +1,40 @@
|
||||
# history-hooks 적용 및 토큰 사용량 집계
|
||||
|
||||
**소요 시간**: 약 40분
|
||||
**Context 사용량**: input ~13k / output ~142k tokens (cache 포함)
|
||||
**이슈**: #0
|
||||
|
||||
---
|
||||
|
||||
## 작업 내용
|
||||
|
||||
### 1. history_hooks 리뷰 및 프로젝트 적용
|
||||
- `history_hooks/` 디렉토리의 5개 파일 분석
|
||||
- `.claude/hooks/`에 복사: `guard-history-fields.py`, `guard-history-fields.sh`, `guard-history-reminder.sh`, `session-context.sh`, `path.json`
|
||||
- `session-context.sh`, `guard-history-reminder.sh`를 `$CLAUDE_PROJECT_DIR` 기반 절대경로로 수정
|
||||
- `.claude/settings.json`에 3개 훅 이벤트 등록:
|
||||
- `PostToolUse(Edit|Write)` → `guard-history-fields.sh`
|
||||
- `Stop` → `guard-history-reminder.sh`
|
||||
- `UserPromptSubmit` → `session-context.sh`
|
||||
- `docs/history/` 디렉토리 생성
|
||||
|
||||
### 2. 일자별 토큰 사용량 집계
|
||||
- `~/.claude/projects/d--MYCLAUDE-PROJECT-abcvideo/*.jsonl` 파싱
|
||||
- `requestId` 중복 제거 후 날짜별 집계
|
||||
- 결과: 총 974 요청, input 12,556 / output 135,445 / cache_create 3,655,924 / cache_read 91,618,473
|
||||
|
||||
### 3. Gitea 이슈 코멘트 등록
|
||||
- `kimminsung/dronevideoplayer#1`에 일자별 토큰 사용량 테이블 코멘트 추가
|
||||
|
||||
### 4. guard-history-reminder.sh 수정
|
||||
- 기존: `exit 0` → stderr 메시지만 출력, Claude 그냥 종료
|
||||
- 변경: 오늘 날짜 히스토리 파일 없으면 `exit 2`로 Claude 종료 차단 → 히스토리 작성 강제
|
||||
|
||||
## 산출물
|
||||
- `.claude/hooks/guard-history-fields.py`
|
||||
- `.claude/hooks/guard-history-fields.sh`
|
||||
- `.claude/hooks/guard-history-reminder.sh` (수정)
|
||||
- `.claude/hooks/session-context.sh`
|
||||
- `.claude/hooks/path.json`
|
||||
- `.claude/settings.json` (Stop, UserPromptSubmit 훅 추가)
|
||||
- `docs/history/` 디렉토리
|
||||
@@ -0,0 +1,53 @@
|
||||
# 2026-04-02 프레임 동기화, 렌더링 최적화, 터널링
|
||||
|
||||
## 작업 개요
|
||||
- 프레임 번호 불일치(Python vs abcvideo) 원인 분석 및 수정
|
||||
- StationOverlay 렌더링 끊김 개선
|
||||
- 외부 접속 터널링(cloudflared) 설정
|
||||
- 빨간선 하단 클리핑 수정
|
||||
- 좌표계/카메라 파라미터 시스템 설명
|
||||
|
||||
## 완료 항목
|
||||
|
||||
### 프레임 동기화 수정
|
||||
- **원인**: VFC 측정에서 첫 프레임 카운트 포함 → 29.97fps 영상이 31fps로 오감지
|
||||
- 29.97fps → 1초에 30프레임, elapsed≈1.001s, frameCount=31 → round(31/1.001)=**31fps**
|
||||
- `useFrameStep.ts`: VFC 첫 호출은 startTime만 기록, 카운트 제외
|
||||
- `VideoPlayer.tsx`: 프레임 표시를 `VIDEO_FPS=30000/1001(29.97)`로 계산 → Python SRT FrameCnt와 일치
|
||||
- `StationOverlay.tsx`: 드론 프레임 매칭을 `currentTime`(초) 기반으로 변경 (fps 오감지 무관)
|
||||
|
||||
### 빨간선 하단 클리핑 수정
|
||||
- 기존: 한쪽 끝점만 화면 밖이어도 세그먼트 전체 스킵
|
||||
- 수정: Cohen-Sutherland trivial reject (양쪽 다 같은 방향 밖일 때만 스킵)
|
||||
- SCREEN_M: 30 → 200으로 증가
|
||||
|
||||
### StationOverlay 렌더링 최적화
|
||||
- 드론 경로(흰색 선) 완전 제거
|
||||
- 좌표 변환(224점 proj4)을 useEffect로 이동 → 드론 프레임 변경 시만 실행
|
||||
- RAF 루프: 캐시된 픽셀 좌표만 읽어 draw (계산 없음 → 60fps 유지)
|
||||
- ResizeObserver로 캔버스 크기 별도 관리
|
||||
|
||||
### 기본값 ON
|
||||
- `showStations` 초기값 `false` → `true`
|
||||
|
||||
### 터널링
|
||||
- `vite.config.ts`: `allowedHosts: true` 추가
|
||||
- cloudflared 명령: `cloudflared tunnel --url http://localhost:5173`
|
||||
- PowerShell 환경변수 설정: `$env:VIDEOS_DIR="..."; npm run dev:server`
|
||||
|
||||
### 좌표 시스템 설명
|
||||
- 타원체고(h) = 표고(H) + 지오이드고(N=25.449m) 설명
|
||||
- EPSG:5186 TM 투영 원리
|
||||
- center.csv vs 측점_XY값.csv 역할 차이
|
||||
- 드론 카메라 회전 행렬(Rz×Rx×Ry) → 핀홀 투영 전체 흐름 설명
|
||||
|
||||
## 기술 부채 / 남은 이슈
|
||||
- 재생 중 끊김이 완전히 제거되지 않음 (데이터 자체가 29.97fps 이산 → 근본적 한계)
|
||||
- server-side geoMatch.ts는 여전히 구형 ENU 근사 사용 (클라이언트 렌더링에는 미사용)
|
||||
|
||||
## Git
|
||||
- 커밋: `30bec66` (frontend 브랜치)
|
||||
- push 완료
|
||||
|
||||
**소요 시간**: 180분
|
||||
**Context 사용량**: input 180k / output 18k tokens
|
||||
@@ -0,0 +1,27 @@
|
||||
# 동영상 로드 실패 (서버 중지) 복구
|
||||
|
||||
**이슈**: #0
|
||||
**소요 시간**: 10분
|
||||
**Context 사용량**: input 45k / output 3k tokens
|
||||
|
||||
## 증상
|
||||
좌측 동영상 목록에서 항목 클릭 시 "로드할 수 없음" 에러.
|
||||
|
||||
## 원인
|
||||
- 백엔드 서버(pm2 프로세스 `abcVideo`, 포트 55173)가 **stopped 상태**였음.
|
||||
- 프론트엔드와 API가 동일 포트(노드 정적 서빙)에서 동작하므로, 서버가 죽으면 `/api/stream/:videoId` 요청이 전부 실패 → 영상 로드 불가.
|
||||
- pm2 error 로그상 과거 `EADDRINUSE`(포트 3030 충돌)로 재시작 실패 이력이 있었고 stopped로 남아 있었음.
|
||||
- 코드(URL 인코딩, Range Request, 한글·괄호 포함 파일명 처리)는 정상.
|
||||
|
||||
## 조치
|
||||
1. `pm2 restart abcVideo --update-env` → online 복구
|
||||
2. 검증
|
||||
- `GET /api/videos` → `하행)회덕-대전조차장.MP4` 반환
|
||||
- `GET /api/stream/...` (Range 0–1MB) → HTTP 206, 1MB 수신
|
||||
- `GET /api/meta/...` → HTTP 200
|
||||
3. `pm2 save`로 프로세스 목록 저장
|
||||
|
||||
## 후속 참고
|
||||
- `FFmpeg not found in PATH` 경고 → HLS 변환/프레임 추출은 FFmpeg 설치 후 동작 (즉시 재생은 영향 없음).
|
||||
- `client/vite.config.ts` 프록시 타깃이 `localhost:3030`인데 실제 서버 포트는 55173.
|
||||
프로덕션 정적 서빙에는 무관하나, Vite 개발 서버(`npm run dev`) 사용 시 API 프록시 불일치로 동작 안 함 → 개발 모드 사용 시 포트 정정 필요.
|
||||
@@ -0,0 +1,76 @@
|
||||
# 폴더 임포트 v2.0 데이터 형상 전환
|
||||
|
||||
**소요 시간**: 약 16분 (기능개발 에이전트 기준)
|
||||
**Context 사용량**: input ~추정 / output ~107k tokens (기능개발 에이전트 subagent_tokens 106,989 기준)
|
||||
|
||||
## 작업 개요
|
||||
|
||||
폴더 선택 시 로딩하는 지리정보 데이터를 v1.0 형상에서 v2.0 형상으로 전환했다.
|
||||
v1.0(POI_위경도값/측점_위경도값/타원체고/center.csv 의존)을 더 이상 지원하지 않고,
|
||||
실측 납품 폴더 구조(드론 base.csv + building/ 하위 번호 CSV + route.json 보정)에 맞춘 v2.0 전용 파서로 전면 재작성했다.
|
||||
|
||||
### 작업 배경 (v1.0 → v2.0)
|
||||
- v1.0은 별도 가공 산출물(`POI_위경도값`, `측점_위경도값`, `center.csv`, 타원체고 컬럼)에 의존 → 실제 납품 폴더와 컬럼/인코딩이 불일치
|
||||
- v2.0 실데이터는 한 폴더에 드론 CSV(UTF-8)와 측량 CSV(EUC-KR)가 혼재하고, 중심선(center.csv)이 없음
|
||||
- 따라서 인코딩 자동 감지 + 번호 CSV 직접 파싱 + 측점 기반 중심선 생성으로 구조를 단순화
|
||||
|
||||
## 변경 파일과 핵심 내용
|
||||
|
||||
### client/src/types/geo.ts
|
||||
- `DirectionChange` 타입 신설: `{ station, from, to, atSeconds }`
|
||||
- `RouteStructure` 에 `lengthM?`, `category?` 추가 (type 유니온 `bridge|tunnel|station` 유지, 구교 → `bridge`)
|
||||
- `FolderGeoData` 에 `structures: RouteStructure[]`, `directionChanges: DirectionChange[]` 추가
|
||||
|
||||
### client/src/utils/geoData.ts (전면 재작성, v2.0 전용)
|
||||
- 인코딩 자동 감지 `decodeBytes(buf)`: 앞 3바이트 `EF BB BF` → UTF-8, 아니면 EUC-KR, 잔여 `U+FEFF` 제거
|
||||
- `readCsv(file)` 시그니처 단순화 (인코딩 인자 제거)
|
||||
- `makeFieldIndexer`: 헤더명 `indexOf` 실패 시 위치 인덱스 폴백
|
||||
- 신규 파서: `parseStations` / `parsePois` / `parseStructures` / `buildCenterlineFromStations` / `mergeStructures`
|
||||
- v1.0 코드 제거: `POI_위경도값`/`측점_위경도값`/타원체고 식별, `center.csv` 로더, 인코딩 하드코딩 분기
|
||||
|
||||
### client/src/store/geoStore.ts
|
||||
- `structures`, `directionChanges` 적재
|
||||
|
||||
### client/src/stationbar/StationBar.tsx
|
||||
- 구조물 마커 소스를 `routeMeta.structures` → `geoStore.structures`(CSV 유래) 우선으로 전환
|
||||
- 위치 로직(station → lat/lon → 이름매칭 → mileage)은 lat/lon 지원으로 그대로 활용
|
||||
|
||||
## 컬럼 매핑 (실파일 검증)
|
||||
|
||||
| 구분 | 파일 | 인코딩 | 매핑 |
|
||||
|------|------|--------|------|
|
||||
| 드론 | `<base>.csv` | UTF-8 | frame_cnt/latitude/longitude/altitude/yaw/pitch/roll/focal_len — 변경 없음 |
|
||||
| 측점 | building/`01)측점.csv` | EUC-KR | title←측점, z←Z좌표, lat, lon, type=station; 비고에서 DirectionChange 추출(mm:ss→초) |
|
||||
| POI(1) | 루트 `<base>_POI.csv` | UTF-8 | title / category_clean / lat / lon, z=0 |
|
||||
| POI(2) | building/`02)지장물.csv` | EUC-KR | 명칭 / category='지장물' / lat / lon / z←Z좌표 |
|
||||
| 구조물 | `03)교량`(bridge) + `04)터널`(tunnel) + `06)구교`(bridge, category='구교') | — | name / lengthM←연장(m) / lat / lon; 위경도 범위(33~39 / 124~132) 밖 깨진 행 필터 |
|
||||
| 중심선 | center.csv 없음 | — | 측점 mileage 정렬 폴리라인으로 생성 |
|
||||
| 보정 | route.json | — | `mergeStructures` 가 name 매칭으로 offset/station/좌표 override + 미존재 항목 추가 (선택적 보정) |
|
||||
|
||||
## 결정 사항
|
||||
- **v2.0 전용화**: v1.0 지원 코드를 제거하고 v2.0 형상만 로딩
|
||||
- **구조물 = CSV 자동 + route.json 보정**: 03/04/06 CSV에서 자동 수집하고 route.json은 선택적 override/augment 레이어로만 사용
|
||||
- **측점 중심선**: center.csv 부재 → 측점 mileage 정렬 폴리라인을 중심선으로 사용
|
||||
- **비고 방향전환**: 측점 비고 텍스트에서 상/하행 전환 + 시각(mm:ss)을 정규식으로 추출하여 `DirectionChange` 생성
|
||||
|
||||
## 실데이터 검증값 (하행)회덕-대전조차장 데이터, v2.0)
|
||||
- 인코딩: 드론/POI/05)출입문/06)구교 = UTF-8(BOM), 01~04 = EUC-KR (BOM 자동감지로 일괄 처리)
|
||||
- directionChanges 2건: 157K930 상행→하행 @125s, 158K340 하행→상행 @75s
|
||||
- 측점 47개
|
||||
- POI 137개 (`<base>_POI` 114 + 지장물 23)
|
||||
- 구조물: 교량 12 (깨진 1행 필터) + 터널 2 + 구교 9
|
||||
|
||||
## 미구현 / 후속 과제
|
||||
- **05)출입문번호.csv 파싱 스킵**: 운영데이터로 재생바 아이콘이 과도해져 기본 표시 제외. 파싱 자체를 생략(Timeline 렌더는 bridge/tunnel/station만). 추후 필요 시 `parseDoors` 추가.
|
||||
- **directionChanges 와이어링**: 타입/스토어까지 적재 완료. StationBar `barSegments` 의 방향전환은 현재 연속 체이니지(chain) 자동 판정 로직을 유지하고, `directionChanges` 강제 분할은 미적용. 자동 판정이 영상과 일치하므로 회귀 위험 회피 목적의 후속 과제로 둠.
|
||||
|
||||
## 검수 포인트
|
||||
- 한글 인코딩 깨짐 없음 (BOM 자동감지)
|
||||
- 깨진 행 마커 제외 (위경도 범위 밖 필터)
|
||||
- route.json 보정 동작
|
||||
- RoutePanel / RouteInfoOverlay 는 POI category로 교량·터널을 필터 → v2.0에서는 structures가 분리되어 해당 목록이 비어 보일 수 있음(정상, 구조물은 StationBar로 표시)
|
||||
|
||||
## 빌드 결과
|
||||
- `nvm use 20` → `npm run build -w client` → `tsc && vite build`
|
||||
- ✓ built (215 modules), 에러 0
|
||||
- chunk > 500kB 경고는 기존 video.js 특성 (정상)
|
||||
@@ -0,0 +1,84 @@
|
||||
# 구조물 시설종별(1종/2종/3종) 표시 필터
|
||||
|
||||
**소요 시간**: 약 9분 (기능개발 에이전트 기준)
|
||||
**Context 사용량**: input ~추정 / output ~100k tokens (subagent_tokens 100,287 기준)
|
||||
|
||||
## 작업 개요
|
||||
|
||||
구조물(교량/터널/구교)의 **시설종별(1종/2종/3종)** 에 따라 재생바(StationBar)와 경로 패널(RoutePanel)의 표시를 필터링하는 기능을 추가했다.
|
||||
플레이어에 1/2/3종 체크박스를 두어 사용자가 종별 노출을 즉시 토글할 수 있고, 선택 상태는 localStorage 에 지속된다.
|
||||
|
||||
### 작업 배경 (사용자 요청)
|
||||
- 교량 등 **3종 구조물이 너무 많아** 재생바·패널이 혼잡 → 기본적으로 3종은 숨기고 싶다는 요청
|
||||
- 시설종별 1종/2종/3종을 **체크박스로 켜고 끄는** 토글 UI 요구
|
||||
- 기본값은 **1종 ☑ / 2종 ☑ / 3종 ☐** (3종만 기본 숨김)
|
||||
|
||||
## 변경 / 신설 파일과 핵심 내용
|
||||
|
||||
### client/src/types/geo.ts (변경)
|
||||
- `RouteStructure` 에 `grade?: string` 추가 (시설종별 원문 문자열, 예: `"3종"`, `"2종"`, `"기타"`)
|
||||
|
||||
### client/src/utils/geoData.ts — parseStructures (변경)
|
||||
- `pushFrom` spec 에 `gradeFallback`(시설종별 컬럼 위치 인덱스) 추가
|
||||
- 시설종별을 헤더명 `'시설종별'` `indexOf` 우선 + 위치 인덱스 폴백으로 읽음
|
||||
- **교량**: 시설종별 = `idx 1`
|
||||
- **터널**: 시설종별 = `idx 5`
|
||||
- **구교(06)**: 시설종별 컬럼 없음 → `gradeFallback` 미지정 → `grade` = `undefined`
|
||||
- 읽은 값은 `trim`, 빈 문자열이면 `undefined` 로 정규화
|
||||
|
||||
### client/src/store/settingsStore.ts (신설)
|
||||
- `zustand` + `persist` 미들웨어, 저장 키 `'ghivideo.settings'`
|
||||
- `gradeFilter` 기본값 `{ '1종': true, '2종': true, '3종': false }`
|
||||
- `setGradeFilter(grade, visible)` 액션
|
||||
- `KNOWN_GRADES`(`['1종','2종','3종']`) 와 공유 표시 규칙 `isGradeVisible` export
|
||||
- `merge` 로 저장본에 누락된 키를 기본값으로 보충 (버전 호환)
|
||||
|
||||
### 공유 표시 규칙 — isGradeVisible
|
||||
```
|
||||
!grade || !KNOWN_GRADES.includes(grade) || gradeFilter[grade]
|
||||
```
|
||||
- `grade` 없음(구교 등) → **항상 표시**
|
||||
- `grade` 가 KNOWN 미포함('기타'/'인상' 등) → **항상 표시**
|
||||
- 1/2/3종 → `gradeFilter[grade] === true` 일 때만 표시
|
||||
|
||||
### client/src/stationbar/StationBar.tsx (변경)
|
||||
- `storeStructuresRaw` 를 `isGradeVisible` 로 필터(`useMemo`, `gradeFilter` 구독) → `structureMarks` 가 필터된 목록을 사용
|
||||
|
||||
### client/src/components/overlay/RoutePanel.tsx (변경)
|
||||
- `filteredPois` 에 `isGradeVisible` 조건 추가(동일 규칙, `gradeFilter` 구독)
|
||||
|
||||
### client/src/components/player/VideoPlayer.tsx (변경)
|
||||
- 좌하단 컨트롤 그룹의 **배속 행 아래**에 `"시설등급"` 라벨 + 1/2/3종 체크박스 3개
|
||||
- 노출 조건: `showVideoControls && geoLoaded`
|
||||
- 각 체크박스는 `setGradeFilter` 에 바인딩 → 즉시 토글 반영
|
||||
|
||||
## 시설종별 파싱 요약
|
||||
|
||||
| 구분 | 파일(03/04/06) | 시설종별 컬럼 | 결과 |
|
||||
|------|----------------|----------------|------|
|
||||
| 교량 | 03)교량 | `idx 1` (헤더 '시설종별' 우선) | grade = "3종"/"기타" 등 |
|
||||
| 터널 | 04)터널 | `idx 5` (헤더 '시설종별' 우선) | grade = "2종" 등 |
|
||||
| 구교 | 06)구교 | 없음 | grade = undefined (항상 표시) |
|
||||
|
||||
## 기본값 / 지속
|
||||
|
||||
- 기본값: **1종 ☑ / 2종 ☑ / 3종 ☐**
|
||||
- localStorage 키 `'ghivideo.settings'` 로 선택 상태 지속 (`zustand persist`)
|
||||
- UI 위치: 플레이어 좌하단 컨트롤(배속 행 아래), `showVideoControls && geoLoaded` 일 때 노출
|
||||
|
||||
## 실데이터 분포 (하행)회덕-대전조차장 데이터, v2.0, 좌표필터 적용 후)
|
||||
|
||||
- **교량 12건**: 시설종별 3종 ×10, 기타 ×2 ("인상" 1건은 깨진 좌표로 이미 필터됨)
|
||||
- **터널 2건**: 시설종별 2종 ×2
|
||||
- **구교 9건**: 시설종별 컬럼 없음 → `grade` undefined(항상 표시)
|
||||
- 기본값(1종☑2종☑3종☐) 적용 시: **3종 교량 10건 숨김**, 기타 교량 2건 · 터널 2건(2종) · 구교 9건 표시
|
||||
|
||||
## 빌드 결과
|
||||
|
||||
- `nvm use 20` → `npm run build -w client` → `tsc && vite build`
|
||||
- 에러 0 (기존 chunk > 500kB 경고만, video.js 특성상 정상)
|
||||
|
||||
## 검수 포인트 / 후속 과제
|
||||
|
||||
- **1종 데이터 부재**: 이 데이터엔 1종 구조물이 없어 1종 체크박스의 실제 토글 효과는 1종이 포함된 다른 데이터로 검증 권장(필터 구조 자체는 완성)
|
||||
- **StationOverlay 미적용**: 영상 위 측점 마커(StationOverlay)는 `structures` 를 사용하지 않으므로 필터 미적용 — 요구 범위(재생바/패널)대로
|
||||
@@ -0,0 +1,742 @@
|
||||
# POI/측점 투영 오차 개선 — 개발자 기술 문서
|
||||
|
||||
**날짜**: 2026-06-22
|
||||
**대상 데이터**: 하행)회덕-대전조차장 (v2.0)
|
||||
**수정 파일**: `client/src/utils/geoProjection.ts`, `client/src/utils/geoData.ts`, `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
이 문서는 GPS POI/측점/중심선 오버레이의 위치 오차를 개선한 작업 기록이다.
|
||||
**개발자가 코드를 직접 이해하고 추가 수정할 수 있도록** 배경·원인·코드·튜닝 방법을 모두 담는다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 투영 파이프라인 개요 (먼저 읽기)
|
||||
|
||||
영상 위에 측점/POI/선로중심선을 그리는 전체 흐름:
|
||||
|
||||
```
|
||||
CSV/SRT 드론 텔레메트리(프레임별 lat/lon/alt/yaw/pitch/roll)
|
||||
│ geoData.ts: parseDroneFrames / parseStations / parsePois / parseStructures
|
||||
▼
|
||||
geoStore (Zustand) ── frames / stations / pois / centerline / origin
|
||||
│
|
||||
▼
|
||||
StationOverlay.tsx
|
||||
├─ smoothFrame(): 드론 자세를 ±N프레임 이동평균으로 평활 (GPS/자세 노이즈 제거)
|
||||
├─ startLabelPrecompute(): 전 프레임 × 전 측점/POI 의 화면좌표를 미리 계산 → labelMap
|
||||
├─ RAF 루프: labelMap 조회 + 프레임 간 보간 + 화면 EMA → canvas 그리기
|
||||
│
|
||||
▼ (좌표 변환 핵심)
|
||||
geoProjection.ts: projectPoint() / toCameraCoords() + pixelFromCamera()
|
||||
1) WGS84(lat/lon) → EPSG:5186 TM(easting/northing, m) [proj4]
|
||||
2) 대상·드론의 상대 ENU 벡터 rel = target - drone (m)
|
||||
3) 회전행렬 R_b2w = Rz(-yaw)·Rx(pitch)·Ry(roll), R_w2c = R_align·R_b2wᵀ
|
||||
4) 카메라좌표 (Xc,Yc,Zc) = R_w2c·rel
|
||||
5) 핀홀 투영 px = 0.5 + (Xc/Zc)·(f/sensorW), py = 0.5 + (Yc/Zc)·(f/sensorH)
|
||||
```
|
||||
|
||||
투영 공식 자체는 Python 원본 `pythonsource/advanced_tuner_v2.py` 와 1:1 일치한다 (검증 완료).
|
||||
**오차는 공식이 아니라 "공식에 들어가는 입력값(표고·평활지연)" 에서 발생**했다.
|
||||
|
||||
데이터 사실(검증됨):
|
||||
- 영상: 3840×2160, **29.97fps(=30000/1001)** → 코드의 `VIDEO_FPS` 와 일치 (프레임 동기화 OK)
|
||||
- focal_len=24.0(고정), pitch=-29.8°(고정), roll=0(고정) → intrinsic/자세 입력 OK
|
||||
- yaw: GPS 진행방향(course-over-ground)과 짐벌 yaw 차이 중앙값 **-0.74°** → yaw 바이어스 없음
|
||||
|
||||
---
|
||||
|
||||
## 1. [수정] 측점 표고를 잘못된 컬럼에서 읽던 문제 (수직 오차 ~31°)
|
||||
|
||||
### 증상
|
||||
측점/중심선이 실제보다 훨씬 아래(또는 화면 밖)에 그려짐. 구버전에서 측점 158K500이
|
||||
`py=1.225`(화면 아래로 이탈)로 계산됨.
|
||||
|
||||
### 원인
|
||||
`building/01)측점.csv` 에는 표고 컬럼이 **두 개**다:
|
||||
- `Z좌표` (col 3): **로컬 좌표계 높이 ≈ 0** (예: 0.0235)
|
||||
- `Z좌표_한국` (col 9): **EPSG:5186 실제 정표고 ≈ 41.5m**
|
||||
|
||||
기존 `parseStations` 가 `Z좌표`(≈0)를 읽어, 측점/중심선이 표고 0에 놓였다.
|
||||
드론 abs_alt(타원체고)=84m 와의 수직차가 -84m(부각 42.7°)로 과대 계산됨.
|
||||
|
||||
### 검증 (proj4 직접 계산, 157K900, frame 0, 수평거리 91.3m)
|
||||
| 사용 표고 | 드론과 수직차 | 부각 |
|
||||
|---|---|---|
|
||||
| Z좌표≈0 (기존) | -84.2m | 42.7° ❌ |
|
||||
| Z좌표_한국 41.5 (정표고) | -42.7m | 25.1° |
|
||||
| 정표고+지오이드 ≈67 (타원체고) | **-17.7m** | **11.0°** ✅ |
|
||||
|
||||
드론 SRT `rel_alt`=16.85m 와 타원체고 사용 시 수직차(-17.7m)가 일치 → 정답 확인.
|
||||
|
||||
### 코드 (`client/src/utils/geoData.ts`, `parseStations`)
|
||||
```diff
|
||||
const iTitle = fi('측점', 0);
|
||||
- const iZ = fi('Z좌표', 3);
|
||||
+ // Z좌표(col 3)는 로컬좌표(≈0). 실제 표고는 Z좌표_한국(EPSG:5186, 정표고)에 있다. 없으면 폴백.
|
||||
+ const iZKorea = fi('Z좌표_한국');
|
||||
+ const iZ = iZKorea >= 0 ? iZKorea : fi('Z좌표', 3);
|
||||
```
|
||||
> 중심선은 `buildCenterlineFromStations()` 가 측점 z를 그대로 쓰므로 이 한 줄로 함께 교정됨.
|
||||
> **다른 데이터에서 컬럼명이 다르면** `fi('Z좌표_한국')` 의 문자열만 바꾸면 된다.
|
||||
|
||||
---
|
||||
|
||||
## 2. [수정] 정표고 → 타원체고 변환 (지오이드 보정) 추가
|
||||
|
||||
### 원인
|
||||
측점의 `Z좌표_한국`(41.5m)은 **정표고(EL, 해수면 기준)** 이고, 드론 `abs_alt`(84m)는
|
||||
**타원체고(GRS80 기준)**. 둘은 datum이 달라 **지오이드고(N)** 만큼 차이난다.
|
||||
대전 지역 N ≈ 25.8m (= 84.244 − 16.853 − 41.59, KNGeoid 값과 일치).
|
||||
|
||||
정표고+25.8 = 타원체고 → 드론 alt와 같은 기준이 됨.
|
||||
|
||||
### 설계
|
||||
지오이드 보정은 **모든 대상(측점/POI/중심선)** 에 공통 적용되므로 투영 레이어
|
||||
(`geoProjection.ts`)에서 대상 고도에 더한다. 튜닝 가능하도록 `CameraParams` 필드로 노출.
|
||||
|
||||
### 코드 (`client/src/utils/geoProjection.ts`)
|
||||
```diff
|
||||
// CameraParams 인터페이스
|
||||
+ geoidOffset: number; // 지오이드고(m): 대상 정표고(EL)→타원체고. 드론 abs_alt와 datum 일치. 대전≈25.8
|
||||
|
||||
// DEFAULT_CAMERA_PARAMS
|
||||
+ geoidOffset: 25.8,
|
||||
|
||||
// projectPoint() 와 buildRelEnu() 두 곳 모두
|
||||
- const stEnu = geoToEnu(targetLat, targetLon, targetAlt, ...);
|
||||
+ const stEnu = geoToEnu(targetLat, targetLon, targetAlt + (params.geoidOffset ?? 0), ...);
|
||||
```
|
||||
> 수학: `rel[2] = (targetZ + geoid) − droneAlt`. geoid를 키우면 대상이 위로(멀리) 이동.
|
||||
> **다른 지역**은 패널 "지오이드" 슬라이더로 조정 (국내 22~30m). 코드 기본값은
|
||||
> `DEFAULT_CAMERA_PARAMS.geoidOffset` 에서 변경.
|
||||
|
||||
---
|
||||
|
||||
## 3. [수정] 화면 EMA 과평활로 인한 라벨 지연 (~1.65초)
|
||||
|
||||
### 증상
|
||||
카메라가 움직일 때 라벨이 뒤늦게(약 1.6초) 따라옴 → 이동 중 좌우/상하 어긋남.
|
||||
|
||||
### 원인
|
||||
`StationOverlay.tsx` 의 화면좌표 EMA(지수이동평균) 계수 `emaAlpha` 기본값이 **0.01**.
|
||||
RAF 60fps에서 시정수 ≈ 16.7ms/0.01 ≈ **1.65초**. 입력은 이미 `smoothFrame(±10fr)`로
|
||||
평활되는데 화면 EMA까지 과하게 걸려 이중 평활 → 큰 지연.
|
||||
|
||||
### 코드 (`client/src/components/overlay/StationOverlay.tsx`)
|
||||
```diff
|
||||
- const [emaAlpha, setEmaAlpha] = useState(0.01);
|
||||
- const emaAlphaRef = useRef(0.01);
|
||||
+ // 0.01은 과평활(지연~1.65s). 입력이 smoothFrame으로 이미 평활되므로 화면 EMA는 가볍게(0.4≈지연25ms).
|
||||
+ const [emaAlpha, setEmaAlpha] = useState(0.4);
|
||||
+ const emaAlphaRef = useRef(0.4);
|
||||
```
|
||||
> EMA 동작: `disp += (target − disp) · α`. α↑ = 반응 빠름/덜 부드러움, α↓ = 느림/부드러움.
|
||||
> 떨림이 심하면 패널 "EMA α" 를 0.2~0.3으로, 지연이 거슬리면 0.5~0.7로.
|
||||
|
||||
---
|
||||
|
||||
## 4. [수정] POI가 "실제보다 가까이(아래)" 뜨는 문제 — POI 전용 표고 보정
|
||||
|
||||
### 증상
|
||||
드론 시점에서 라벨(POI)이 실제 위치보다 가까이/아래에 뜸. 특히 건물(회덕화물역 등).
|
||||
|
||||
### 원인 (이건 "버그"가 아니라 데이터 한계)
|
||||
화면의 POI들(회덕화물역·회덕역주차장·국도17호선 등)은 웹소스 `_POI.csv` 출신으로
|
||||
**표고 컬럼이 아예 없다**. 또 오버레이는 POI 고도를 `nearestCL().z`(가장 가까운 선로점 표고)로
|
||||
덮어쓴다 → **모든 POI가 "선로 지면 높이"로 가정**됨.
|
||||
|
||||
계산(frame 0, 드론 지면 위 17m): 회덕화물역 거리 27m, 수직갭 17m → 부각 32°, py=0.55.
|
||||
이 값은 **지면 점 기준으로는 기하학적으로 정확**하다. 그러나
|
||||
1. 건물은 높이가 있어 지면 마커가 건물 몸체보다 아래에 보이고,
|
||||
2. 주변 지반이 선로보다 높으면(예: +4~5m) 그만큼 더 아래/가깝게 보임.
|
||||
|
||||
per-POI 실제 표고(DEM/건물높이)가 없어 자동 보정은 불가. → **튜닝 핸들**을 제공.
|
||||
|
||||
### 설계
|
||||
- 선로 중심선/측점은 표고가 정확하므로 **건드리면 안 됨**.
|
||||
- 따라서 지오이드(전체 적용)와 별개로, **POI에만** 더하는 `poiZOffset` 을 신설.
|
||||
- 적용 위치: 투영 레이어가 아니라 `StationOverlay` 의 **POI 루프에서만** poiZ에 가산.
|
||||
|
||||
### 코드
|
||||
`client/src/utils/geoProjection.ts` (필드/기본값만 추가, 투영식엔 미사용):
|
||||
```diff
|
||||
+ poiZOffset: number; // POI 전용 표고 보정(m): POI/구조물 마커만 들어올림. 측점·중심선 미적용.
|
||||
...
|
||||
+ poiZOffset: 0, // 기본 0 (건물높이/지형차에 맞춰 패널에서 조정)
|
||||
```
|
||||
`client/src/components/overlay/StationOverlay.tsx` (POI 루프):
|
||||
```diff
|
||||
for (const poi of allPoi) {
|
||||
- const poiZ = nearestCL(poi.lat, poi.lon)?.z ?? poi.z;
|
||||
+ // 측점/중심선과 달리 POI만 poiZOffset 적용 (건물 높이/지형차 보정)
|
||||
+ const poiZ = (nearestCL(poi.lat, poi.lon)?.z ?? poi.z) + (currentParams.poiZOffset ?? 0);
|
||||
const cc = toCameraCoords(drone, poi.lat, poi.lon, poiZ, currentParams, worldOrigin);
|
||||
```
|
||||
패널 슬라이더 추가(지오이드 아래):
|
||||
```
|
||||
<ParamRow label="POI 표고" value={params.poiZOffset} min={-10} max={40} step={0.5} unit="m" .../>
|
||||
```
|
||||
> **사용법**: POI가 건물보다 아래/가깝게 뜨면 "POI 표고" 를 +방향으로 올린다(회덕 부근 +4~5m 권장).
|
||||
> 기본 0은 기존 동작 유지(선로지면). 도로처럼 실제 지면에 있는 POI는 0이 맞을 수 있어,
|
||||
> 데이터 성격에 따라 절충해 맞춘다.
|
||||
>
|
||||
> **추가 개선 여지(미구현)**: 구조물/지장물 CSV가 실제 표고를 제공하면 per-POI z를 쓰도록
|
||||
> `nearestCL().z` 대신 해당 z를 사용. 현재 building CSV의 `Z좌표` 는 로컬값(≈0~16)이라 부적합.
|
||||
|
||||
---
|
||||
|
||||
## 5. [수정] 먼 POI가 "선로 안"에 어긋나 보이는 문제 — POI 거리 제한
|
||||
|
||||
### 증상
|
||||
88자원(고물상) 등 일부 POI가 선로 위/안에 표시됨. "안 맞는 POI가 좀 있다."
|
||||
|
||||
### 원인 (핵심)
|
||||
**멀리 있는 POI는 부각이 0°에 수렴해 화면 "지평선(소실점)"에 몰린다.** 그 소실점은
|
||||
선로가 사라지는 지점과 같다 → 먼 POI들이 전부 선로 위에 겹쳐 찍힌다.
|
||||
|
||||
측점 160K900 프레임에서 화면에 잡히는 POI 9개 중 8개가 **240~312m 거리**이고 전부
|
||||
`py≈0.0`(지평선), px 0.2~0.6에 몰려 있었다. 88자원은 노선기준 ~2.3km로 더 멀다.
|
||||
가까운 POI(태경자동차 121m)는 정상 위치. 즉 **"안 맞는 POI = 먼 POI"** 다.
|
||||
|
||||
추가로 이 POI들은 표고가 없어 선로 지면 높이에 놓여 정확히 소실선 위에 앉는다(§4 참조).
|
||||
|
||||
### 설계
|
||||
거리 컷이 가장 단순·확실. 카메라좌표의 슬랜트 거리 `√(Xc²+Yc²+Zc²)` 가 임계값을
|
||||
넘으면 그 POI를 사전계산 단계에서 제외(라벨 자체를 안 만듦). 측점/중심선은 미적용
|
||||
(측점은 선로 위라 소실점으로 가도 자연스러움).
|
||||
|
||||
### 코드 (`client/src/components/overlay/StationOverlay.tsx`)
|
||||
상태 추가:
|
||||
```diff
|
||||
+ const [maxPoiDist, setMaxPoiDist] = useState(200); // POI 표시 거리 상한(m)
|
||||
+ const maxPoiDistRef = useRef(200);
|
||||
+ useEffect(() => { maxPoiDistRef.current = maxPoiDist; }, [maxPoiDist]);
|
||||
```
|
||||
precompute 시그니처에 인자 추가 + POI 루프에서 컷:
|
||||
```diff
|
||||
- const startLabelPrecompute = useCallback((currentParams, currentSmoothHalf) => {
|
||||
+ const startLabelPrecompute = useCallback((currentParams, currentSmoothHalf, currentMaxPoiDist) => {
|
||||
...
|
||||
+ const MAX_POI_DIST = currentMaxPoiDist;
|
||||
for (const poi of allPoi) {
|
||||
const cc = toCameraCoords(...);
|
||||
if (cc.Zc < CLIP_Z) continue;
|
||||
+ const slant = Math.sqrt(cc.Xc*cc.Xc + cc.Yc*cc.Yc + cc.Zc*cc.Zc);
|
||||
+ if (slant > MAX_POI_DIST) continue; // 먼 POI 제외
|
||||
```
|
||||
두 호출부와 effect 의존성에 `maxPoiDist` 추가, 패널에 슬라이더 추가:
|
||||
```
|
||||
<ParamRow label="POI 거리" value={maxPoiDist} min={30} max={1000} step={10} unit="m" .../>
|
||||
```
|
||||
> **튜닝**: 기본 200m. 너무 휑하면 ↑(300~500), 먼 라벨이 거슬리면 ↓(100~150).
|
||||
> 슬랜트 거리(3D)라 드론 높이(~35m) 포함이지만 100m+에선 수평거리와 거의 같다.
|
||||
> **측점에도 같은 컷을 주고 싶으면** 측점 루프(`for (const st of allSt)`)에 동일 패턴 추가.
|
||||
|
||||
---
|
||||
|
||||
## 6. [결정] 선로변 POI 좌표 오차는 코드로 보정 불가 (데이터 한계)
|
||||
|
||||
### 분석
|
||||
거리 필터 적용 후에도 88자원 라벨이 선로 위에 보인 케이스 분석:
|
||||
- 스크린샷 프레임은 측점 160K700 부근, 이때 88자원은 **90m 거리**(먼 POI 아님 → 거리필터 통과 정상).
|
||||
- 88자원 좌표(36.381059,127.420448)의 **선로 중심선까지 수직거리 = 13.5m** (nearest 측점 160K800, 31m).
|
||||
- 즉 좌표 자체가 선로 바로 옆 → 90m 거리에서 13.5m 측면오프셋은 화면상 선로와 겹쳐 보임.
|
||||
|
||||
### 결론
|
||||
- POI **수평 위치(px)** 는 좌표의 bearing으로 정확히 결정됨(투영 정확). 투영 버그 아님.
|
||||
- KAKAO/VWORLD POI는 주소 기반 지오코딩 → ±수십m 오차. 철도 인접 업체(고물상 등)는
|
||||
좌표가 선로 옆에 찍힘. **올바른 좌표 없이는 코드로 못 고침.**
|
||||
|
||||
### 방침 (사용자 결정)
|
||||
- "선로 옆 실제 POI일 수 있으니 숨기지 말고, 줄일 수 있는 오차만 최소화."
|
||||
- → 근접 POI 숨김/거리 하향 **미적용**. 거리 필터 200m 유지.
|
||||
- → 줄일 수 있는 체계오차(수직 datum/지오이드/지연/소실점몰림/FOV)는 이미 적용·튜닝 가능.
|
||||
- → 개별 POI 좌표를 바로잡으려면 별도 보정좌표 파일이 필요(향후 과제).
|
||||
|
||||
---
|
||||
|
||||
## 7. [분석] KMZ `절대고도`는 상수 — POI 표고 개선에 사용 불가
|
||||
|
||||
### 배경
|
||||
`2구간_14.회덕-대전조차장_하행.kmz`(=doc.kml)에 각 POI의 description 표가 있고
|
||||
`절대고도` 필드가 있음. POI별 실측 지반고도면 §4의 poiZOffset 추정을 대체할 수 있어 검토.
|
||||
|
||||
### 분석 (python으로 doc.kml 전수 파싱)
|
||||
- Placemark 100개, 절대고도 보유 83개. **전부 67.391m (고유값 1개), Z좌표=0.**
|
||||
- 67.391 = 노선 출발점 지면 타원체고 (= 드론 frame0 abs_alt 84.244 − rel_alt 16.853).
|
||||
- 즉 **출발점 고도 하나를 모든 POI에 일괄 기입**한 값. per-POI 실측 아님.
|
||||
|
||||
### 왜 쓰면 안 되나
|
||||
현재 방식(POI = 가장 가까운 선로점 표고 + 지오이드)은 선로 상승(정표고 41.6→57.5m)을
|
||||
따라 POI 고도가 67→83m로 변함 → 지형 상승 반영. KMZ 상수(67.391)는 후반 POI를 ~16m
|
||||
낮게 만들어 **현재 방식보다 부정확**. (드론 abs_alt가 84→120으로 상승하는 것과도 부합)
|
||||
|
||||
### 결론
|
||||
- KMZ `절대고도`는 datum 검증용으로만 유효(지오이드 25.8 재확인). 표고 개선엔 미사용.
|
||||
- 진짜 개선엔 **POI별 실측 Z** 또는 **DEM** 필요. KMZ 좌표는 _POI.csv와 동일이라
|
||||
88자원 등 수평 오차도 변화 없음.
|
||||
- 코드 변경 없음.
|
||||
|
||||
---
|
||||
|
||||
## 8. [전수조사] 데이터 폴더 전체 탐색 — 실측 고도원(DEM) 부재 확인
|
||||
|
||||
### 조사 대상 (데이터 폴더 전 파일)
|
||||
`*.MP4 / *.srt / *.csv(드론) / *_POI.csv / building/01~06 / *.kmz / *.qgz / *_구조물추출_Z.xlsx`
|
||||
|
||||
### 고도(Z) 데이터 전수 비교
|
||||
| 소스 | 고도 필드 | 값 | 판정 |
|
||||
|---|---|---|---|
|
||||
| building/01)측점.csv | `Z좌표_한국` | 정표고 41.6→57.5m (**가변, 실측**) | ✅ 유일한 실제 가변 고도. **이미 사용** |
|
||||
| building/01)측점.csv | `Z좌표` | 로컬 ≈0 | 무효 |
|
||||
| *_POI.csv / KMZ / xlsx(교량·터널·지장물) | `절대고도` | **상수 67.391** | 출발점 고도 일괄, 무효 |
|
||||
| xlsx 구교 | `z` | 67.391 + 로컬오프셋(비고 '기존값') | 무효 |
|
||||
| xlsx(교량·터널) | `Z좌표` | 로컬 2~22 | 구조물 상대높이, 지형 아님 |
|
||||
| *.srt | `rel_alt` | 이륙점 기준 | 지형프로파일 아님(상수지형) |
|
||||
| *.qgz(QGIS) | 래스터/DEM | **없음** (벡터 레이어만 참조) | 없음 |
|
||||
|
||||
### .qgz 분석
|
||||
참조 GeoPackage `14회덕-대전조차장_layers.gpkg`(폴더에 없음)의 레이어:
|
||||
`드론경로_버퍼_150m, 드론경로_라인/포인트, 출입문, 구조물_교량/터널, 지장물`.
|
||||
→ **DEM 없음.** 또한 **POI 선정 기준이 '드론경로 150m 버퍼'** 임을 확인.
|
||||
|
||||
### 결론
|
||||
- POI별 실측 지형고도(DEM)는 데이터셋에 **존재하지 않음**.
|
||||
- 실제 가변 고도는 측점 `Z좌표_한국` 뿐이며 **현재 POI 고도 추정(최근접 측점 표고+지오이드)에 이미 사용 중** → 수직은 데이터 한계상 최선.
|
||||
- 좌표는 모든 파일이 동일(지오코딩) → 88자원류 수평오차 개선 불가.
|
||||
|
||||
### 적용 변경
|
||||
데이터 설계(150m 버퍼)에 맞춰 POI 거리 필터 기본값 **200 → 150m**.
|
||||
```diff
|
||||
- const [maxPoiDist, setMaxPoiDist] = useState(200);
|
||||
- const maxPoiDistRef = useRef(200);
|
||||
+ const [maxPoiDist, setMaxPoiDist] = useState(150); // '드론경로_버퍼_150m'와 정렬
|
||||
+ const maxPoiDistRef = useRef(150);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. [검증] 구글어스 '지면고도' vs 현재 방식 — 0.2m 일치 (변경 불필요)
|
||||
|
||||
### 배경
|
||||
사용자가 KMZ를 구글어스에 import → POI 클릭 시 `지면 고도` 값이 표시됨을 확인
|
||||
(예: 신대천교(복) = 41.4566m). "파일에 per-POI 고도가 있다"는 취지.
|
||||
|
||||
### 사실관계
|
||||
- **KMZ 파일**: `절대고도=67.391`(상수)만 저장. (§7과 동일, 변함없음)
|
||||
- **구글어스**: 클릭 시 자체 DEM에서 `지면고도`(정표고)를 **실시간 계산**해 표시.
|
||||
파일 값이 아니라 구글 지형 데이터.
|
||||
- **현재 앱**: 측량 측점 `Z좌표_한국`(정표고)로 POI 지면고도 추정.
|
||||
|
||||
### 수치 검증
|
||||
| POI | 구글어스 지면고도 | 현재(최근접 측점 정표고) | 차이 |
|
||||
|---|---|---|---|
|
||||
| 신대천교(복) | 41.4566m | 41.26m (158K400) | **0.2m** |
|
||||
|
||||
→ 90m 거리에서 0.2m = 0.13°, ~6px(2160p), 무시 가능. 현재 방식이 POI별로 변하는
|
||||
지면고도(40.8~80.5m, 지형상승 반영)를 이미 정확히 산출 → **구글어스 DEM과 동급**.
|
||||
|
||||
### 결론
|
||||
- 수직(고도)은 이미 측량 측점 기반으로 구글어스와 0.2m 일치 → **개선 여지 없음, 코드 변경 없음**.
|
||||
- 공개 DEM(SRTM 30m, ±수m)을 끌어와도 측량 측점보다 정밀하지 않아 regression 위험.
|
||||
- 잔여 88자원류는 **수평 좌표(지오코딩) 오차** → 고도 데이터로 해결 불가(올바른 좌표 필요).
|
||||
|
||||
---
|
||||
|
||||
## 10. [기능] POI 마우스 드래그 위치 보정 + 내보내기/가져오기
|
||||
|
||||
### 목적
|
||||
지오코딩 오차로 어긋난 POI(88자원 등)를 **마우스로 끌어** 올바른 위치에 놓고, 그
|
||||
보정을 파일로 저장 → 다음 로드 시 자동 반영. (수평+수직 동시 보정)
|
||||
|
||||
### 동작 원리
|
||||
1. **편집 모드** ON → 오버레이 canvas가 포인터 입력을 받음(`pointer-events`).
|
||||
2. POI 마커 **드래그** → 놓은 화면 픽셀을 **역투영**해 월드 좌표(lat/lon/z) 복원.
|
||||
- 2D→3D 깊이 모호성은 **슬랜트 거리 유지**로 해소(드래그 시작 시 거리 고정).
|
||||
- 따라서 한 번의 드래그로 **수평·수직 동시** 보정.
|
||||
3. 결과를 `title` 키로 store에 저장(`setPoiOverride`) → `pois` 재적용 → labelMap 재계산.
|
||||
4. **내보내기**: `<base>_poi_overrides.json` 다운로드. **가져오기**: 파일 선택 적용.
|
||||
5. 그 파일을 **데이터 폴더에 두면** 다음 폴더 로드 시 `parsePoiOverrides`가 자동 적용.
|
||||
|
||||
### 역투영 수학 (검증됨: Zc>0 왕복오차 ≈0)
|
||||
`geoProjection.ts worldFromPixel()`:
|
||||
```
|
||||
dir_cam ∝ ((px-0.5-cx0)·sW/f, (py-0.5-cy0)·sH/f, 1) // 픽셀→카메라 방향
|
||||
cam = dir_cam/|dir_cam| · range // 거리 유지
|
||||
rel = R_w2cᵀ · cam // 회전 역
|
||||
E = droneE + offX + rel.x, N = droneN + offY + rel.y
|
||||
z = droneAlt + offZ + rel.up − geoid // 정표고로 복원(투영이 geoid 재가산)
|
||||
(lat,lon) = TM⁻¹(E,N)
|
||||
```
|
||||
> 화면에 보이는 POI(Zc>0)만 드래그 가능 → 역투영 정확. 카메라 뒤(Zc<0)는 해당 없음.
|
||||
|
||||
### 보정 POI의 고도 처리
|
||||
보정된 POI는 precompute에서 **자신의 z(정표고)를 그대로 사용**(`overridesRef`로 판별).
|
||||
미보정 POI는 기존대로 `nearestCL.z + poiZOffset`. (둘 다 투영에서 geoid 가산)
|
||||
|
||||
### 수정 파일
|
||||
- `types/geo.ts`: `PoiOverride`, `PoiOverrideMap`, `FolderGeoData.poiOverrides`.
|
||||
- `utils/geoProjection.ts`: `worldFromPixel()` (역투영).
|
||||
- `utils/geoData.ts`: `parsePoiOverrides()`(폴더 JSON 로드), `applyPoiOverrides()`(title 매칭 교체), `loadFolderGeoData`에 연결.
|
||||
- `store/geoStore.ts`: `basePois`/`poiOverrides` 상태, `setPoiOverride`/`clearPoiOverride`/`setPoiOverrides`, 로드시 적용.
|
||||
- `components/overlay/StationOverlay.tsx`: 편집모드 토글, canvas 포인터 핸들러(hit-test→드래그→drop 역투영), 드래그 피드백 렌더, 패널 UI(편집/내보내기/가져오기), 보정 POI z 처리, override 변경 시 재계산.
|
||||
|
||||
### 사용법 (사용자)
|
||||
1. 카메라 파라미터 패널 → **"✎ 편집 모드"** 클릭.
|
||||
2. POI를 끌어 올바른 위치에 놓기(영상 일시정지 권장 — 자세가 안정적).
|
||||
3. **"내보내기"** → `<base>_poi_overrides.json` 저장.
|
||||
4. 그 파일을 **데이터 폴더**(MP4 옆)에 복사 → 다음 폴더 로드 시 자동 적용.
|
||||
(또는 **"가져오기"** 로 즉시 적용)
|
||||
|
||||
### 한계/참고
|
||||
- 식별자가 `title` 뿐 → 같은 title POI가 여럿이면 함께 보정됨(현 데이터엔 거의 없음).
|
||||
- drop 후 라벨 이동은 재계산(≈500ms debounce) 뒤 반영.
|
||||
- 측점/중심선/구조물은 편집 대상 아님(POI만).
|
||||
|
||||
---
|
||||
|
||||
## 11. [개선] 라벨 이동 부드럽게 — 부드러운 시간원 + 연속 프레임 보간
|
||||
|
||||
### 증상
|
||||
재생 중 라벨이 매끄럽지 않고 "멈췄다 점프" 하듯 보임.
|
||||
|
||||
### 원인 (2가지)
|
||||
1. StationOverlay에 **거친 `currentTime`** 전달: playerStore.currentTime은 Video.js
|
||||
`timeupdate` 이벤트(약 4~15Hz)로 갱신됨([useVideoPlayer.ts:43]). 반면 VideoPlayer엔
|
||||
이미 60fps 단조보간 시간 `smoothTimeRef`가 있는데 라벨엔 안 쓰고 있었음.
|
||||
2. 보간 `frac`이 **1프레임으로 클램프**(`Math.min(0.999, …)`): currentTime 갱신 간격이
|
||||
1프레임을 넘으면 라벨이 frame+1에서 멈췄다가 다음 갱신 때 여러 프레임 점프.
|
||||
|
||||
### 수정
|
||||
- VideoPlayer: `<StationOverlay timeRef={smoothTimeRef} …>` 추가 (StationBar와 동일 패턴).
|
||||
- StationOverlay RAF: 시간원을 timeRef로 바꾸고 **연속 프레임 인덱스**로 보간:
|
||||
```diff
|
||||
- const frameNum = currentFrameNumRef.current;
|
||||
- const estTime = currentTimeSecRef.current + (now - timeUpdateWallRef.current)/1000;
|
||||
- const frac = Math.min(0.999, estTime*VIDEO_FPS - frameNum); // 1프레임 클램프(멈춤)
|
||||
- const labelsA = get(frameNum), labelsB = get(frameNum+1);
|
||||
+ const estTime = timeRef ? timeRef.current : (prop 기반 폴백);
|
||||
+ const estFrame = estTime * VIDEO_FPS;
|
||||
+ let baseFrame = Math.floor(estFrame), frac = estFrame - baseFrame; // 클램프 없음
|
||||
+ let labelsA = get(baseFrame), labelsB = get(baseFrame+1);
|
||||
+ if (!labelsA) { baseFrame = currentFrameNumRef.current; frac = 0; … } // 갭 폴백
|
||||
```
|
||||
- Props에 `timeRef?: React.MutableRefObject<number>` 추가.
|
||||
|
||||
### 효과
|
||||
- timeRef는 일시정지·시크·배속까지 보정된 60fps 단조시간 → 라벨이 매 프레임 연속 이동.
|
||||
- 1프레임 클램프 제거로 갱신 간격이 길어도 라벨이 끊기지 않고 진행.
|
||||
- 기존 화면 EMA(α=0.4)는 유지 → 프레임 경계 속도변화까지 완만.
|
||||
- 일시정지 시 timeRef 고정 → 라벨 표류 없음(이전 폴백식은 벽시계로 표류 위험).
|
||||
|
||||
### 남은 것 (선택)
|
||||
- 빨간 **중심선**은 아직 `currentTime`(coarse) 기반 + 비보간이라 라벨만큼 매끄럽지 않음.
|
||||
같은 방식(보간 드론 자세로 RAF에서 투영)으로 부드럽게 가능 — 필요 시 후속 작업.
|
||||
|
||||
---
|
||||
|
||||
## 12. [버그픽스] 라벨이 옆으로 튀었다 복귀 — 인덱스 짝짓기 + 이상치 거부
|
||||
|
||||
### 증상
|
||||
재생 중 일부 라벨이 한 프레임 옆으로 확 튀었다가 다음 프레임에 제자리로 돌아옴.
|
||||
|
||||
### 근본 원인 (인덱스 짝짓기 오류)
|
||||
precompute는 프레임별로 `stationLabels`/`poiMarkers`에 **클리핑/화면밖 필터를 통과한 것만
|
||||
push** 한다. RAF 보간은 `labelsA[i]` ↔ `labelsB[i]` 를 **배열 인덱스로 짝지어** 보간했는데,
|
||||
A↔B 사이에 라벨 하나가 필터에 들고나면 인덱스가 밀려 **서로 다른 라벨끼리 보간** →
|
||||
좌표가 확 튀고, 다음 프레임에 정렬되며 복귀.
|
||||
|
||||
### 수정 (StationOverlay.tsx)
|
||||
1. **title로 짝짓기** (근본 해결): 프레임 B 라벨을 `Map<title, entry>` 로 인덱싱 후
|
||||
`stA.title` 로 조회. 항상 같은 라벨끼리 보간.
|
||||
```diff
|
||||
- const stB = labelsB?.stationLabels[i]; // 인덱스 짝짓기(필터 변하면 어긋남)
|
||||
+ const stB_byTitle = new Map((labelsB?.stationLabels ?? []).map(s => [s.title, s]));
|
||||
+ const stB = stB_byTitle.get(stA.title); // title 짝짓기
|
||||
```
|
||||
2. **이상치 거부 필터** (요청 + 잔여 GPS/투영 스파이크 대비): 표시좌표 갱신 시 이전 위치
|
||||
대비 점프가 임계(`REJECT_DIST=0.12`, 정규화) 초과면 **갱신 무시(이전 위치 유지)**.
|
||||
시크/재등장에 갇히지 않도록 연속 `MAX_REJECT_FRAMES=8` 후 수용.
|
||||
```ts
|
||||
function smoothStep(prev, tx, ty, alpha) {
|
||||
if (!prev) return { x:tx, y:ty, rej:0 };
|
||||
const d = Math.hypot(tx-prev.x, ty-prev.y);
|
||||
if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES)
|
||||
return { x:prev.x, y:prev.y, rej:prev.rej+1 }; // 이상치 무시
|
||||
return { x:prev.x+(tx-prev.x)*alpha, y:prev.y+(ty-prev.y)*alpha, rej:0 };
|
||||
}
|
||||
```
|
||||
3. **사라진 라벨 상태 정리**: 이번 프레임에 안 그린 라벨은 displayed 맵에서 삭제 →
|
||||
재등장 시 옛 좌표가 남아 거부로 늦게 뜨는 것 방지(새 위치에서 시작).
|
||||
|
||||
### 튜닝
|
||||
- 정상 팬 이동은 60fps에서 프레임당 ≪0.12 라 영향 없음. 스파이크(0.2~0.5)만 걸러짐.
|
||||
- 너무 빠른 팬에서 잠깐 머뭇하면 `REJECT_DIST` ↑ 또는 `MAX_REJECT_FRAMES` ↓.
|
||||
|
||||
---
|
||||
|
||||
## 13. [기능] 구조물(교량/터널/구교)도 영상 오버레이에 표출
|
||||
|
||||
### 배경
|
||||
"신대천교(복)이 왜 안 보이나?" → 신대천교(복)은 `03)교량.csv`(구조물)에만 있고
|
||||
`_POI.csv`/지장물엔 없음. 오버레이는 측점+POI만 그려 구조물은 미표시였음
|
||||
(구조물은 RoutePanel 미니맵 전용이었음).
|
||||
|
||||
### 수정 (StationOverlay.tsx)
|
||||
- `useGeoStore(s => s.structures)` 구독, 구조물을 GeoPoint 형태로 변환해
|
||||
`allStructuresRef` 에 보관(이모지: category 교량🌉/터널🚇/구교🌉).
|
||||
```ts
|
||||
allStructuresRef.current = storeStructures
|
||||
.filter(s => typeof s.lat==='number' && typeof s.lon==='number')
|
||||
.map(s => ({ title:s.name, category:s.category ?? (s.type==='tunnel'?'터널':'교량'),
|
||||
lat:s.lat, lon:s.lon, z:0, type:'poi' }));
|
||||
```
|
||||
- precompute에서 `allPoi = allPoisRef.current.concat(allStructuresRef.current)` →
|
||||
구조물도 POI와 동일 파이프라인(평면거리필터·title보간·EMA·드래그보정)으로 표시.
|
||||
- 재계산 트리거에 `storeStructures` 의존 추가.
|
||||
|
||||
### 효과/참고
|
||||
- 드론 평면거리 60m 이내의 교량/터널/구교가 라벨로 표출됨.
|
||||
- 측점선 토글(`visible`)·POI 거리필터·POI 드래그 보정이 동일 적용.
|
||||
- lat/lon 없는(측점기반) 구조물은 제외.
|
||||
|
||||
---
|
||||
|
||||
## 14. [기능] POI 유효거리 앞/옆 분리(비등방 거리필터)
|
||||
|
||||
### 요구
|
||||
"앞쪽 유효거리와 옆쪽 유효거리를 달리해서 POI 표출."
|
||||
|
||||
### 구현
|
||||
평면거리를 진행방향(yaw) 기준 **앞쪽(fwd)·옆쪽(side)** 으로 분해해 각각 임계 적용.
|
||||
- `geoProjection.ts toCameraCoords`: relEnu와 yaw로 성분 계산
|
||||
```ts
|
||||
const yaw = toRad(camera.yaw + params.yawOffset);
|
||||
cc.fwd = relEnu[0]*sin(yaw) + relEnu[1]*cos(yaw); // +앞
|
||||
cc.side = relEnu[0]*cos(yaw) - relEnu[1]*sin(yaw); // 좌우
|
||||
```
|
||||
- `StationOverlay`: `maxPoiDist` → `maxPoiFront`/`maxPoiSide`(상태·ref·슬라이더 2개).
|
||||
필터: `if (cc.fwd > MAX_FRONT || |cc.side| > MAX_SIDE) continue;`
|
||||
- 기본값 앞 60m / 옆 40m. 패널 "POI 앞쪽"(10~1000) · "POI 옆쪽"(5~500) 슬라이더.
|
||||
|
||||
### 효과
|
||||
- 진행방향으로는 멀리(앞 큰 값), 좌우 클러터는 좁게(옆 작은 값) 식으로 독립 조절.
|
||||
- 측점/구조물도 동일 POI 파이프라인 → 함께 적용.
|
||||
|
||||
---
|
||||
|
||||
## 15. [변경] 구조물 표출 위치 분리 — 교량·터널=스테이션바 / 구교=영상
|
||||
|
||||
### 요구
|
||||
`building/`의 **교량(03)·터널(04)** 은 하단 **스테이션바**에, **나머지(구교 06 + 지장물 02 +
|
||||
_POI)** 는 **영상 화면**(POI 거리설정 적용)에 표출.
|
||||
|
||||
### 사실관계
|
||||
- 스테이션바(StationBar)는 이미 `geoStore.structures`를 `s.type`으로 판정해 교량/터널을
|
||||
표출 중. 단 **구교(06)는 type='bridge'라 교량에 섞여 바에 같이 표출**되고 있었음.
|
||||
- §13에서 임시로 교량/터널/구교를 모두 영상에 넣었음 → 본 변경으로 정리.
|
||||
|
||||
### 수정
|
||||
- `StationOverlay.tsx`: 영상 구조물은 **category==='구교'만** 포함(교량/터널 제외).
|
||||
```diff
|
||||
- .filter(s => typeof s.lat==='number' && typeof s.lon==='number') // 모든 구조물
|
||||
+ .filter(s => s.category==='구교' && typeof s.lat==='number' && typeof s.lon==='number')
|
||||
```
|
||||
- `stationbar/StationBar.tsx`: 구조물 루프에서 **구교 제외**(교량/터널만 바에 유지).
|
||||
```diff
|
||||
for (const s of storeStructures) {
|
||||
+ if (s.category === '구교') continue; // 구교는 영상으로
|
||||
const cat = s.type === 'tunnel' ? '터널' : s.type === 'bridge' ? '교량' : '역사';
|
||||
```
|
||||
|
||||
### 결과 라우팅
|
||||
| 데이터 | 표출 위치 |
|
||||
|---|---|
|
||||
| 03)교량, 04)터널 | 하단 스테이션바 |
|
||||
| 06)구교 | 영상 오버레이(거리필터) |
|
||||
| 02)지장물, _POI.csv | 영상 오버레이(거리필터) |
|
||||
| 01)측점 | 영상(측점 라벨) + 바(측점값) |
|
||||
|
||||
> 구분 키는 `RouteStructure.category`('교량'/'터널'/'구교'). type만으론 구교/교량이
|
||||
> 둘 다 'bridge'라 분리 불가 → category로 라우팅.
|
||||
|
||||
---
|
||||
|
||||
## 16. [기능] 역사(02)지장물_역사.csv) → 스테이션바 표출
|
||||
|
||||
### 요구
|
||||
새 파일 `building/02)지장물_역사.csv`(역=철도역)는 하단 스테이션바에만 표출,
|
||||
그 외(지장물·구교·일반 POI)는 영상에만(거리필터).
|
||||
|
||||
### 분석
|
||||
- 파일 컬럼은 지장물과 동일(명칭,…,lat,lon), EUC-KR. 내용=역(대전조차장역·회덕화물역).
|
||||
- 스테이션바 Timeline은 이미 `category==='역사'`를 **원형 아이콘**으로 렌더(Timeline.tsx:181).
|
||||
- 함정1: 파일명 `02)지장물_역사.csv`가 `02)지장물`을 포함 → 기존 지장물 파서가 오집 가능.
|
||||
- 함정2: 회덕화물역은 `_POI.csv`에도 `철도역`으로 존재 → 바·영상 중복 노출.
|
||||
|
||||
### 수정 (geoData.ts / StationOverlay.tsx)
|
||||
- `parseHistoricStations()`: `지장물_역사` 파일 → `RouteStructure{type:'station', category:'역사', lat, lon}`.
|
||||
`loadFolderGeoData`에서 `structures`에 concat → 바가 역사로 표출(영상은 '구교'만 표시해 제외됨).
|
||||
- `parsePois` 지장물 매칭에서 **'역사' 포함 파일 제외**(오집 방지).
|
||||
- 영상 POI 목록에서 **category '철도역'/'역사' 제외** → 역사 중복(영상) 제거, 바에만 표출.
|
||||
|
||||
### 결과 라우팅(최종)
|
||||
| 데이터 | 표출 |
|
||||
|---|---|
|
||||
| 03)교량, 04)터널 | 스테이션바 |
|
||||
| **02)지장물_역사 (역사)** | **스테이션바(원형 아이콘)** |
|
||||
| 06)구교 | 영상(거리필터) |
|
||||
| 02)지장물, _POI(철도역 제외) | 영상(거리필터) |
|
||||
| _POI 철도역 | 제외(역사로 바에 표출) |
|
||||
|
||||
---
|
||||
|
||||
## 17. [진단+영속화] 멀리 있는 POI가 어긋나 보이고 가까이 가면 맞는 현상
|
||||
|
||||
### 증상
|
||||
멀리서 본 POI가 실제 지도 위치와 차이가 크고, 드론이 접근(천정점)하면 정확해짐.
|
||||
재생 중 POI가 "뒤로 밀려나며" 수렴.
|
||||
|
||||
### 진단 (기하학)
|
||||
- **천정점에서 정확 + 멀수록 오차 증가**는 **표고(z) 오차가 아님**.
|
||||
표고 오차의 화면 영향 Δθ ≈ Δz·D/(D²+H²) 는 D≈H(드론높이~35m)에서 최대이고
|
||||
멀수록(D≫H) 감소. 가까이(~30m)서 맞으면 표고는 정상 → DEM(50m)은 오히려 틀림(z≈42 정상).
|
||||
- **천정점=0, 지평선으로 증가**는 **카메라 각도(yaw) 미세편차**의 특징(POI가 yaw축인
|
||||
천정 방향에 있을 때 0, 지평선에서 최대). 가까이선 고부각이라 작고, 멀리선 크게 벌어짐.
|
||||
- 즉 **짐벌 각도 보정 오프셋(Yaw±/Pitch±)이 0** 이라 잔여 편차 미보정 상태(Python 튜너가
|
||||
찾던 값). 1회 보정하면 전 거리에서 정렬됨.
|
||||
|
||||
### 수정 — 보정값 영속화 (StationOverlay.tsx)
|
||||
오프셋이 새로고침마다 0으로 초기화되던 문제 → **데이터셋(baseName)별 localStorage 저장·복원**:
|
||||
```ts
|
||||
// 로드: ghivideo:calib:<baseName> → params + 표시설정 복원 (baseName당 1회)
|
||||
// 저장: params/maxPoiFront/maxPoiSide/smoothHalf/emaAlpha 변경 시 디바운스 저장
|
||||
```
|
||||
→ Yaw±/Pitch± 등 한 번 맞추면 유지되어 "처음부터 맞게" 표시됨.
|
||||
|
||||
### 사용자 보정 절차
|
||||
1. 멀리 어긋난 POI가 보이는 프레임에서 일시정지.
|
||||
2. **Yaw±**(좌우 어긋남) / **Pitch±**(상하 어긋남)를 조금씩 움직여 POI를 실제 위치에 맞춤.
|
||||
3. 자동 저장됨(다음 로드 시 적용). (브라우저별. 휴대는 POI 내보내기와 별개 — 필요 시 확장.)
|
||||
|
||||
### 한계
|
||||
- 자동 보정은 정답 화면좌표(대응점)가 없어 불가 → 1회 수동 보정 필요.
|
||||
- 향후: 먼 POI를 드래그하면 그 편차로 yaw/pitch 오프셋을 역산하는 캘리브레이션 가능.
|
||||
|
||||
---
|
||||
|
||||
## 18. [수정] 멀리서 앞으로 밀려보이는 슬라이드 = 표고 오차 → 드래그 표고 역산
|
||||
|
||||
### 재진단 (앞 §17 정정)
|
||||
증상: POI가 진행방향으로 **앞에(멀리) 표시**되고, 드론이 다가갈수록 **뒤로 밀려** 실제
|
||||
위치에 수렴. → **표고(z) 오차가 맞음.** 지면 어긋남은 **Δ앞 = D·Δz/(H−Δz)** 로 거리 D에
|
||||
**비례**(천정점 0). §17에서 "각도 문제"로 본 건 화면 각도만 봐서 틀렸고, 사용자 관점의
|
||||
지면 거리로 보면 정확히 표고. 방향상 **z가 과대**(POI를 둑·다리로 솟은 선로 표고에 배치 →
|
||||
실지면보다 높음)라 앞으로 밀림.
|
||||
|
||||
### KML 절대고도 재확인 → 사용 불가
|
||||
KMZ/doc.kml 96개 placemark 전부 `절대고도=67.391/392`(상수), coords z=0.
|
||||
**per-POI 실지면고도 아님**(출발점 고도 일괄). 주차장은 그 상당값에서 ~6~9m 더 낮아져야
|
||||
하므로 상수로는 못 고침.
|
||||
|
||||
### 수정 — 드래그로 표고 z 역산 (lat/lon 고정)
|
||||
데이터에 per-POI 지면고도가 없으므로, **드래그로 POI를 실제 지면에 내려놓으면 그 세로
|
||||
위치에 맞는 z를 역산**해 저장. lat/lon은 고정(수평은 보통 정확) → 한 번 맞추면 전 거리에서
|
||||
정렬되어 슬라이드가 사라짐. 보정은 override 파일/영속화로 유지.
|
||||
- `geoProjection.ts solveZForPixelY()`: py=0.5+cy0+(Yc/Zc)(f/sH), Yc·Zc 가 z에 1차 →
|
||||
`z = z0 + (R·Zc0 − Yc0)/(dYc − R·dZc)`, `R=(py−0.5−cy0)·sH/f`, dYc=−b2w[2][2], dZc=b2w[2][1].
|
||||
(왕복검증: z=42→py→solveZ=42.000. 화면 아래로 끌면 z↓=지면 낮춤.)
|
||||
- `StationOverlay.tsx`: 드래그 종료 시 `worldFromPixel`(거리유지) → `solveZForPixelY`(lat/lon
|
||||
고정·z풀이)로 교체. 드롭 세로위치로 z 산출 후 override 저장.
|
||||
|
||||
### 사용
|
||||
편집 모드 → 어긋난 POI를 **실제 지면 위치(보통 아래로)로 드래그** → 표고가 맞춰져 슬라이드
|
||||
제거. 한 번 하면 영속화(§10 override + §17 calib 저장).
|
||||
|
||||
### 한계/대안
|
||||
- per-POI라 다수 POI는 수동. 공통적으로 높으면 "POI 표고"(poiZOffset) 음수로 일괄 하향 가능.
|
||||
- 90m DEM은 절벽/구조물 인접에서 부정확(주차장 50m 오판) → 자동 적용 안 함.
|
||||
|
||||
---
|
||||
|
||||
## 19. [기능] DEM 표고 자동적용 (좌표→지면고도) + 보정 자동저장
|
||||
|
||||
### 배경
|
||||
"POI 좌표를 아니 절대고도(지면고도)를 구할 수 있지 않나?" → 맞음. 데이터엔 per-POI 지면이
|
||||
없지만(KML/SRT는 상수 67.391로 환원) **좌표로 DEM을 조회**하면 됨.
|
||||
|
||||
### 검증 (open-meteo, Copernicus 90m)
|
||||
| POI | DEM | 측점(현재) | 차이 | 해석 |
|
||||
|---|---|---|---|---|
|
||||
| 회덕화물역/주차장 등 선로변 | ~42 | ~41.4 | +0.6 | 거의 동일(정상) |
|
||||
| 경부고속도로 | 35 | 40.8 | −5.8 | 도로는 더 낮음(정상) |
|
||||
| 갑천(하천) | 29 | 50.5 | −21.5 | 하천 훨씬 낮음(정상) |
|
||||
중앙값 차이≈0(선로변), 골짜기 지형은 적절히 하향. DEM 정표고 ≈ 측점 정표고(신대천교 42.0 vs 41.3) → datum 일치, +geoid(25.8)로 타원체고 변환.
|
||||
|
||||
### 구현 (StationOverlay.tsx)
|
||||
- **"🌐 DEM 표고 자동적용"** 버튼: 현재 POI+구조물 좌표를 100개씩 묶어 open-meteo
|
||||
`/v1/elevation` 배치 조회(CORS 허용 확인) → 각 POI override `{lat,lon,z:DEM}` 설정.
|
||||
override z는 정표고로 저장되어 투영 시 geoid 가산(선로 POI와 동일 경로).
|
||||
- **보정 자동저장**: poiOverrides 를 `ghivideo:poiov:<baseName>` localStorage 에 저장·복원
|
||||
(드래그/DEM 결과가 새로고침해도 유지). 파일 내보내기/가져오기는 그대로(휴대용).
|
||||
|
||||
### 한계
|
||||
- 공개 90m DEM이라 **절벽·구조물 인접(예: 신대지구 주차장)은 부정확**(주차장을 50m로 과대) →
|
||||
그런 소수는 **드래그 표고 보정**(§18)으로 미세조정.
|
||||
- 고정밀이 필요하면 국토지리정보원 5m DEM/VWorld 고도API(키 필요)로 교체 가능.
|
||||
|
||||
### 사용
|
||||
편집 모드 → **"DEM 표고 자동적용"** 1클릭 → 전 POI 지면고도 적용(자동저장). 멀리서 밀리는
|
||||
현상이 대부분 완화됨. 남는 소수만 드래그.
|
||||
|
||||
---
|
||||
|
||||
## 카메라 파라미터 패널 정리 (현재 노출되는 튜닝값)
|
||||
|
||||
| 슬라이더 | 코드 키 | 의미 | 기본값 |
|
||||
|---|---|---|---|
|
||||
| smooth | `smoothHalf` | 드론 자세 이동평균 반폭(프레임) | 10 |
|
||||
| EMA α | `emaAlpha` | 화면좌표 평활(↑반응/↓부드러움) | 0.4 |
|
||||
| **POI 앞쪽** | `maxPoiFront` | **진행방향 앞쪽 유효거리(평면)** | **60m** |
|
||||
| **POI 옆쪽** | `maxPoiSide` | **진행방향 옆쪽(좌우) 유효거리(평면)** | **40m** |
|
||||
|
||||
> POI 거리필터는 **평면(수평) 거리**(고도차 0 간주)를 **진행방향 앞/옆으로 분해**해 비등방
|
||||
> 적용. `toCameraCoords`가 yaw 기준 `fwd`(+앞), `side`(좌우)를 채우고, precompute가
|
||||
> `fwd > maxPoiFront || |side| > maxPoiSide` 면 제외. (이전: 단일 평면반경 → 앞/옆 분리.)
|
||||
| Yaw/Pitch/Roll ± | `yawOffset/pitch/roll` | 자세 오프셋(SRT값에 가산) | 0 |
|
||||
| off X/Y/Z | `offX/offY/offZ` | 드론 위치 보정(ENU, m) | 0 |
|
||||
| 지오이드 | `geoidOffset` | 정표고→타원체고(전체) | 25.8 |
|
||||
| **POI 표고** | `poiZOffset` | **POI만 표고 보정** | **0** |
|
||||
| f / cx₀ / cy₀ / senW / senH | `focalLen/cx0/cy0/sensorW/sensorH` | 내부표정(FOV/주점) | 24/0/0/36/20.25 |
|
||||
|
||||
---
|
||||
|
||||
## 빌드 & 테스트 절차 (중요)
|
||||
|
||||
이 프로젝트는 **HMR 개발서버가 아니라** pm2가 빌드 산출물(`client/dist`)을 정적 서빙한다.
|
||||
→ 소스 수정 후 **반드시 재빌드해야** 브라우저에 반영된다(새로고침만으론 안 됨).
|
||||
|
||||
```bash
|
||||
# 시스템 node는 v12라 빌드 불가 → nvm node20 사용
|
||||
export PATH=~/.nvm/versions/node/v20.20.2/bin:$PATH
|
||||
cd ~/projects/gitea/b23042/GhiVideo
|
||||
npm run build -w client # tsc + vite build → client/dist 갱신
|
||||
# 서버(pm2 ghiVideo)는 요청마다 dist를 읽으므로 재시작 불필요
|
||||
# 브라우저에서 Ctrl+Shift+R (하드 새로고침)
|
||||
```
|
||||
> 자주 반복하면 `npm run dev -w client`(vite HMR, 포트 5173)가 편함.
|
||||
|
||||
---
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` / `vite build` 통과 (node20).
|
||||
- proj4로 수직 기하 재계산 → 부각 11° 및 POI 부각 32°(지면기준 정확) 확인.
|
||||
- yaw 바이어스 -0.74° (1194 이동샘플) → yaw 오프셋 불필요.
|
||||
|
||||
## 남은 이슈 / 다음 단계
|
||||
- 드론 `abs_alt`가 구간별로 노이즈 있음(지면 위 높이 17m→35m→스파이크 68m). 정밀 수직은
|
||||
per-프레임 abs_alt 신뢰도에 의존. 필요 시 SRT `rel_alt` 병합 고려.
|
||||
- POI 실제 표고(DEM/건물높이) 확보 시 `poiZOffset` 수동튜닝 대신 자동화 가능.
|
||||
- FOV(focal/sensorW)는 16:9 영상 화각이 24mm/36mm 가정과 다를 수 있어 미세 좌우오차 시 튜닝 대상.
|
||||
|
||||
**소요 시간**: 90분
|
||||
**Context 사용량**: input ~180k / output ~25k tokens
|
||||
@@ -0,0 +1,43 @@
|
||||
# 특허 발굴 기술문서 작성 — 스테이션(측점) 기반 주행영상 플레이어 (HTML)
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~70k / output ~12k tokens
|
||||
|
||||
---
|
||||
|
||||
## 작업 개요
|
||||
|
||||
`docs/특허_추출_프롬프트.md`의 5단계 특허 발굴 절차를, 본 프로젝트(GhiVideo)가 실제로 구현한
|
||||
**스테이션(측점)/위치 기반 주행영상 플레이어**에 적용하여 특허 발굴 기술문서를 HTML로 작성했다.
|
||||
|
||||
- 산출물: `docs/특허_스테이션기반_주행영상_플레이어.html` (자체완결 인라인 CSS, 다크 테마, 인쇄 가능)
|
||||
|
||||
## 근거 자료 (실구현 기반)
|
||||
|
||||
프롬프트의 "입력 자료"를 시간기반 추상 개념이 아니라 실제 코드/히스토리에서 추출:
|
||||
- `2026-06-22_POI투영오차-개선.md` — 투영 파이프라인(TM→ENU→카메라→핀홀), 측점표고+지오이드,
|
||||
역투영 드래그 보정, 비등방 거리필터, title짝짓기/이상치거부
|
||||
- `2026-04-01_RoutePanel-미니맵-추가.md` — 측점/㎞ 기반 미니맵·seek
|
||||
- `2026-06-19_v2.0-import전환.md` — 측량 CSV 융합, 방향전환 추출
|
||||
- `2026-04-01_StationOverlay-렌더링분석.md` — labelMap 사전계산 + RAF 보간
|
||||
|
||||
## 문서 구성 (5단계)
|
||||
|
||||
1. **기술요소 인벤토리** E1~E9 (측점색인, geo-registration, DEM-free 고도, 역투영보정, 비등방필터, 노이즈강건 등)
|
||||
2. **특허적격 필터** — 과제/해결수단/비자명성/측정효과 표 + 부적합(E7·E8·E9) 판정
|
||||
3. **발명 후보** A(측점기반 색인·탐색), B(측량측점 표고 DEM-free 정합), C(슬랜트거리보존 역투영 보정), D·E(중)
|
||||
4. **청구항 초안** — B(방법+매체), C(시스템) 독립항 + 종속항, 카테고리 선택 근거
|
||||
5. **선행기술 키워드** 국문/영문 각 5+ , IPC/CPC 추정 분류
|
||||
|
||||
## 결론 (출원 우선순위)
|
||||
|
||||
- 1순위: B+C 결합(정합 정확도 — 부각 42.7°→11°, DEM 대비 0.2m 검증된 효과)
|
||||
- 2순위: A(측점 색인·탐색·위치고정 주석) — 다중회차 동기화 구현범위 [확인 필요]
|
||||
- 3순위: D·E → 1·2순위 종속항으로 흡수
|
||||
- 제외: E7·E8·E9(통상의 데이터 처리, 진보성 곤란)
|
||||
|
||||
## 출력 규칙 준수
|
||||
|
||||
- 추정 부분은 `[확인 필요]` 명시(다중회차 동기화, 위치앵커 주석 데이터모델, 청구 용어)
|
||||
- 특허 부적합 요소는 사유와 함께 명시
|
||||
- 결론에 1~3순위 + 제외 제시
|
||||
@@ -0,0 +1,29 @@
|
||||
# LX(한국국토정보공사) 특허 조사 — 증강현실 / 영상정합 / GIS / 표고
|
||||
|
||||
**소요 시간**: 약 75분
|
||||
**Context 사용량**: input ~95k / output ~12k tokens (추정)
|
||||
|
||||
## 작업 개요
|
||||
한국국토정보공사(LX, Korea Land and Geospatial InformatiX Corp.)가 출원한 KR 특허 중
|
||||
증강현실 / 영상 정합 / GIS 오버레이 / 지오이드·표고 관련 특허 조사.
|
||||
|
||||
## 방법 / 우회
|
||||
- Google Patents XHR API(`/xhr/query`)는 본 환경 IP에서 "Sorry..." 503(봇 차단)으로 직접 접근 불가.
|
||||
- 개별 특허 페이지도 동일하게 차단됨.
|
||||
- **우회 1**: PatentGuru(`patentguru.com/search?q=Korea+Land+and+Geospatial+Informatix`)는 차단 없이
|
||||
LX 출원인 결과 82건(첫 2페이지=20건만 무료 노출) 제공. 제목/번호 확보.
|
||||
- **우회 2**: jina reader 프록시(`r.jina.ai/https://patents.google.com/...`)로 Google Patents 차단 우회.
|
||||
XHR JSON으로 `assignee=한국국토정보공사` total=71건 확인, 첫 10건 풀 메타데이터(출원인/발명자/날짜/PDF) 확보.
|
||||
단, 다량 요청 후 jina 공유 IP도 Google에 차단되어 추가 페이지/키워드 쿼리는 실패.
|
||||
- **우회 3**: `patentimages.storage.googleapis.com` PDF는 차단 없음 → pymupdf로 출원인/초록 직접 검증.
|
||||
|
||||
## 핵심 발견
|
||||
- LX 포트폴리오 핵심은 **공간정보·디지털트윈·지적측량·드론영상·지하공간** 분야.
|
||||
- **증강현실(증강현실) 전용 특허는 확인되지 않음.** LX의 AR 지적측량은 2023.5 음성군 현장 시연
|
||||
(드론·MMS·GNSS·AR) 등 *서비스* 형태이며 AR 특허로 매칭되지 않음.
|
||||
- **지오이드(지오이드) 전용 특허도 확인되지 않음.** 표고 관련은 DTM 특허(KR102199940B1)가 유일한 근접 매칭.
|
||||
검색 시 표고/DEM은 대부분 형제기관인 **국토지리정보원(NGII)** 자료였음.
|
||||
- 영상/모델 정합 매칭: **KR20220135789A**(건물형상정합), **KR20230055533A**(CCTV→공간정보),
|
||||
**KR102376912B1**(정사영상+AI 영상판독), **KR102199940B1**(DTM/디지털트윈).
|
||||
|
||||
자세한 목록과 출처는 최종 보고 참조.
|
||||
@@ -0,0 +1,60 @@
|
||||
# 2026-06-23 — 드론 경로 맨앞(지평선) 오차 컷 + 경로표고 슬라이더 동적 상한
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~155k / output ~17k tokens (추정)
|
||||
|
||||
---
|
||||
|
||||
## 요청
|
||||
1. 드론 궤적이 **맨 앞(진행방향 먼 쪽)** 에서 휘어/튀는 오차. 해결?
|
||||
2. 경로 표고 슬라이더 상한 120의 근거 없음 → **데이터 기반 동적 상한(옵션 1)** 적용.
|
||||
|
||||
---
|
||||
|
||||
## 1. 맨 앞(지평선) 오차 — 전방 거리 컷
|
||||
|
||||
### 원인
|
||||
z=42(지면)면 드론은 지면 위 ~20m뿐. 먼 앞쪽 점일수록 내려보는 각이 얕아져(지평선 근접) GPS·표고 오차가 화면에서 크게 증폭 → 끝이 휘어/튐. 기하적으로 신뢰 불가 구간.
|
||||
|
||||
### 해결 — `StationOverlay.tsx` 드론 경로 빌드 루프
|
||||
진행방향 전방거리(`cc.fwd`)가 `MAX_FWD=200m`를 넘으면 그리기 중단. (i 증가=시간 진행=전방으로 이동, fwd 단조증가 → `break`로 조기 종료, 연산도 절약.)
|
||||
|
||||
```ts
|
||||
const MAX_FWD = 200; // 전방 거리 컷(m)
|
||||
for (let i = lo; i <= hi; i += STEP) {
|
||||
const [la, lo2] = sm(i);
|
||||
const cc = toCameraCoords(drone, la, lo2, Z, params, worldOrigin);
|
||||
if ((cc.fwd ?? 0) > MAX_FWD) break; // 지평선쪽 중단
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
> 더 멀리 보고 싶으면 MAX_FWD↑, 더 깔끔히 자르려면 ↓. (드론 고도가 낮을수록 작게.)
|
||||
|
||||
---
|
||||
|
||||
## 2. 경로 표고 슬라이더 동적 상한 (옵션 1)
|
||||
|
||||
기존 `max={120}`은 근거 없는 라운드 값. → **중심선(centerline) 데이터의 최대 지면표고 + 10m** 로 자동 설정. 데이터 없으면 60 폴백.
|
||||
|
||||
```ts
|
||||
import { ..., useMemo } from 'react';
|
||||
const pathZMax = useMemo(() => {
|
||||
let m = 0;
|
||||
for (const c of storeCenterline) if (typeof c.z === 'number' && c.z > m) m = c.z;
|
||||
return m > 0 ? Math.ceil(m + 10) : 60;
|
||||
}, [storeCenterline]);
|
||||
// 슬라이더:
|
||||
<ParamRow label="경로 표고" ... max={pathZMax} ... />
|
||||
```
|
||||
|
||||
데이터셋이 바뀌어도 그 구간 지형 최고점에 맞춰 상한이 자동 조정됨.
|
||||
|
||||
---
|
||||
|
||||
## 변경 파일
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
- 드론 경로 루프: `MAX_FWD=200` 전방 컷(`cc.fwd` 기준 break)
|
||||
- `useMemo` import + `pathZMax`(중심선 max z +10) + 경로표고 슬라이더 `max={pathZMax}`
|
||||
|
||||
빌드: `index-BzoF-8z_.js` ✓
|
||||
@@ -0,0 +1,32 @@
|
||||
# 2026-06-23 — 드론 궤적 끊김 제거(연속 폴리라인)
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~165k / output ~19k tokens (추정)
|
||||
|
||||
---
|
||||
|
||||
## 요청
|
||||
드론 궤적이 **끊겨(구슬처럼) 보임** → 부드럽게.
|
||||
|
||||
## 원인
|
||||
- 기존: `segFrom`이 점 쌍마다 **조각 선분**을 만들고, RAF에서 조각마다 `moveTo`/`lineTo` → 각 조각이 별도 서브패스라 **연결부가 둥글게 안 이어짐**.
|
||||
- 선이 가늘고(2px, α0.55) 노란 점(r3)이 도드라져 **점선/구슬**처럼 보였다.
|
||||
|
||||
## 해결 — `StationOverlay.tsx`
|
||||
1. **데이터 일원화**: `RenderCache`에서 `dronePathSegs` 제거, **진행방향 순서 점 배열 `dronePathPts`** 만 유지. 빌드 루프는 카메라 앞쪽(`Zc>=CLIP_Z`) 점만 순서대로 수집(`continue`로 뒤쪽 제외, `MAX_FWD` break 유지). `STEP 5→3`(더 촘촘), `BACK 150→60`.
|
||||
2. **RAF: 하나의 연속 폴리라인**으로 그림 — `moveTo(pts[0])` 후 전부 `lineTo`, `lineJoin/lineCap='round'`.
|
||||
- 어두운 밑선(width5, 검정 α0.35·dpa) + 본선(width3, 하늘색 dpa) 2겹 → 배경 위에서도 끊김 없이 선명.
|
||||
3. **점**: r3→**r1.8**, 4개마다 1개만 → 선 흐름 방해 없이 측정점 흔적만.
|
||||
|
||||
```ts
|
||||
// 빌드: 앞쪽 점만 순서대로
|
||||
for (let i=lo;i<=hi;i+=STEP){ const cc=...;
|
||||
if((cc.fwd??0)>MAX_FWD) break;
|
||||
if(cc.Zc<CLIP_Z) continue;
|
||||
const p=pixelFromCamera(cc,params); dronePathPts.push([p.pxRaw,p.pyRaw]); }
|
||||
// RAF: 연속선
|
||||
ctx.lineJoin=ctx.lineCap='round';
|
||||
ctx.beginPath(); ctx.moveTo(pts[0]); for(k=1..) ctx.lineTo(pts[k]); ctx.stroke();
|
||||
```
|
||||
|
||||
빌드: `index-C3L5Fc0X.js` ✓
|
||||
@@ -0,0 +1,35 @@
|
||||
# 2026-06-23 — 선형/드론궤적 화면갱신 부드럽게 (RAF 60fps 보간 투영)
|
||||
|
||||
**소요 시간**: 약 25분
|
||||
**Context 사용량**: input ~210k / output ~26k tokens (추정)
|
||||
|
||||
## 문제
|
||||
드론 궤적·선형(중심선)이 재생 중 "드드득" 끊김.
|
||||
|
||||
## 원인
|
||||
- **라벨(측점/POI)**: RAF에서 `timeRef`(60fps 부드러운 시간) 기반 `estFrame`으로 두 프레임 사이를 **매 프레임 보간** → 부드러움.
|
||||
- **선/궤적**: `renderCacheRef`를 **`currentTime`(timeupdate, ~10Hz 불규칙) → `panelDroneFrame` 변경 → effect 재실행** 경로로만 재생성. 즉 **~10Hz 이산 갱신** → 끊김.
|
||||
|
||||
## 해결 — 선/궤적도 RAF에서 매 프레임 보간 포즈로 직접 투영
|
||||
`StationOverlay.tsx`:
|
||||
|
||||
1. **`buildLines(drone)`** useCallback 신설 — 투영 헬퍼(oc/segFrom) + 중심선 + 드론경로 투영을 캡슐화, refs만 읽어 stable. `{centerlineSegs, dronePathPts}` 반환.
|
||||
2. **`poseAt(estFrame)`** useCallback — 연속 프레임번호에서 앞/뒤 `smoothFrame`을 **선형보간**(yaw는 최단각)해 **서브프레임 드론 포즈** 생성.
|
||||
3. **RAF**: `estFrame`을 (라벨과 공통으로) 먼저 계산 → `buildLines(poseAt(estFrame))`로 선/궤적을 **매 프레임 재투영** → 그림. 라벨 블록은 같은 `estFrame` 재사용.
|
||||
4. **캐시 effect 축소**: `renderCacheRef`는 이제 선/궤적을 보관하지 않고 **나침반/메타데이터(effectiveYaw·hFovRad·count)만**. `RenderCache`에서 `centerlineSegs/dronePathPts` 제거.
|
||||
|
||||
```ts
|
||||
// RAF
|
||||
const estFrame = (timeRef ? timeRef.current : …) * VIDEO_FPS;
|
||||
const lines = buildLines(poseAt(estFrame) ?? smoothFrame(...));
|
||||
// draw lines.centerlineSegs / lines.dronePathPts
|
||||
```
|
||||
|
||||
## 성능
|
||||
- 중심선 = **224점**(center.csv), 드론경로 윈도 = 수백 점. 60fps 투영 ≈ 초당 1.3만~수만 회 → 무시 가능.
|
||||
- 포즈 보간은 `smoothFrame` 2회/프레임(±smoothHalf 평균) — 가벼움.
|
||||
|
||||
## 효과
|
||||
선형·드론궤적이 라벨과 **동일한 60fps 부드러운 시간축**으로 움직여 끊김 제거. 일시정지 시 `estFrame` 고정 → 정지.
|
||||
|
||||
빌드: `index-kWDkGw69.js` ✓
|
||||
@@ -0,0 +1,38 @@
|
||||
# 특허 선행기술 조사 (글로벌 + 한국 KIPRIS) 및 HTML 문서 반영
|
||||
|
||||
**소요 시간**: 약 30분
|
||||
**Context 사용량**: input ~140k / output ~18k tokens (deep-research 워크플로 subagent ~2.5M 별도)
|
||||
|
||||
---
|
||||
|
||||
## 작업 개요
|
||||
|
||||
[특허_스테이션기반_주행영상_플레이어.html](../특허_스테이션기반_주행영상_플레이어.html)의 발명 후보 A·B·C에 대해
|
||||
실제 선행기술을 조사하여 신규성을 검증하고, 결과를 HTML 문서 §6 "선행기술 조사 결과" 섹션으로 추가했다.
|
||||
|
||||
## 조사 방법
|
||||
1. **글로벌**: deep-research 워크플로(5각도 fan-out 검색 → 20소스 fetch → 25주장 3표 교차검증)
|
||||
2. **한국(KIPRIS)**: general-purpose 에이전트 3개 병렬(후보 A/B/C 각각), Google Patents KR 본문 확인
|
||||
|
||||
## 핵심 결론
|
||||
|
||||
| 후보 | 판정 | 가장 가까운 선행기술 |
|
||||
|------|------|---------------------|
|
||||
| 큰 틀 E1·E2 | ❌ 공지 확정 | Esri ArcGIS Pro FMV, US9996976B2, Sensors 2018, Mandli, LineVision, ENSCO VTW |
|
||||
| **A** 측점 탐색 | 🟡 부분 선행(중) | **KR102268318B1**(위치동기 비교재생), **KR100411587B1**(위치→영상 색인) — 광의청구 거절 위험 |
|
||||
| **B** 측점표고 정합 | 🟢 신규성 있음 | DEM-free는 전부 flat-plane 대체, 지오이드 datum 결합 미공개. KR도 동일(LX는 DTM 의존) |
|
||||
| **C** 역투영 보정 | 🟢 신규성 있음 | US9188444B2(depth map 의존, **KR 패밀리 없음**), KR102422292B1(2영상 의존) — 슬랜트거리 보존 미공개 |
|
||||
|
||||
→ **B+C 1순위 유지·강화, A는 2순위 중급으로 하향**(시간축 LRS 비선형 재좌표화+회차간 위치앵커+다중회차 동기 셋으로 좁게 청구 필수).
|
||||
|
||||
## HTML 변경
|
||||
- TOC에 "⑥ 선행기술 조사결과" 추가
|
||||
- §6 신규 섹션: 공지 확정 배너 + 후보별 판정표(글로벌+한국) + 출처/한계 2단 카드
|
||||
- 결론: A 뱃지 "강·요확인"→"중(조사 후 하향)", 선행기술 반영 배너, 한계/footer 갱신
|
||||
|
||||
## 미해소(출원 전 필수)
|
||||
- **KIPRIS 전문 DB 직접검색 미수행**(Google Patents 색인 기반) → 18개월 미공개·미색인 KR 출원 누락 가능. 변리사 전문검색 필요.
|
||||
- 후보 A의 "LRS + 프레임별 카메라자세 AR투영 결합" 선행기술(Esri Roadway/LRS+FMV) 존재 여부 미확인 — A 운명 좌우.
|
||||
- 다중회차 위치동기 비교재생의 실제 구현 범위 확인 필요.
|
||||
|
||||
> 참고: 하위 에이전트가 `docs/history/2026-06-23_LX-한국국토정보공사-특허조사.md`를 별도 생성함.
|
||||
@@ -0,0 +1,30 @@
|
||||
# 2026-06-23 — 화면표시 옵션 현재 튜닝값을 코드 기본값으로
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~175k / output ~21k tokens (추정)
|
||||
|
||||
## 요청
|
||||
사용자가 화면표시 옵션 패널에서 맞춘 현재 값을 코드 기본값으로.
|
||||
|
||||
## 반영 (StationOverlay.tsx useState 기본값)
|
||||
| 항목 | 이전 | 변경 |
|
||||
|------|------|------|
|
||||
| smoothHalf | 10 | **60** |
|
||||
| emaAlpha | 0.4 | **1.0** |
|
||||
| maxPoiFront | 60 | **240** |
|
||||
| maxPoiSide | 40 | **80** |
|
||||
| droneHeightDrop | 20 | **30** |
|
||||
| showDronePath | false | **true** |
|
||||
| dronePathZ | 42 | **78** |
|
||||
| dronePathAlpha | 0.55 | **0.75** |
|
||||
| showCenterline | true | true(유지) |
|
||||
|
||||
## 판단 — poiDroneHeight는 기본 OFF 유지
|
||||
패널에 "POI높이=드론−30m (비교중)"이 ON이었으나, 이는 **비교/테스트 모드**(모든 POI를 실제 DEM/선로표고 대신 드론−N 높이에 배치). 기본 ON이면 그동안 작업한 표고가 무시되므로 **기본 false 유지**, `droneHeightDrop`만 30으로. (원하면 true로 변경 가능.)
|
||||
|
||||
## 참고 — 영속화 범위
|
||||
- `smoothHalf/emaAlpha/maxPoiFront/maxPoiSide`, `params`는 **baseName별 localStorage(calib)** 에 저장되어, 현재 데이터셋은 기존 저장값이 우선. 코드 기본값 변경은 **신규 데이터셋·보정 초기화 시** 적용.
|
||||
- `dronePathZ/dronePathAlpha/showDronePath/droneHeightDrop/poiDroneHeight`는 calib에 미저장 → **코드 기본값이 매 로드 적용**.
|
||||
- (옵션) 이들도 calib 저장에 추가하면 데이터셋별 튜닝이 영구 저장됨 — 필요 시 진행.
|
||||
|
||||
빌드: `index-DHgVDyhN.js` ✓
|
||||
@@ -0,0 +1,111 @@
|
||||
# 2026-06-23 — 드론경로 평활·투명도 + 화면표시 옵션 패널 분리
|
||||
|
||||
**소요 시간**: 약 60분
|
||||
**Context 사용량**: input ~140k / output ~14k tokens (추정)
|
||||
|
||||
---
|
||||
|
||||
## 배경 / 요청
|
||||
|
||||
QGIS 드론 궤적이 계단/Z로 보이는 원인 분석(= GPS 10Hz를 30fps에 복사·유지하여 생긴 계단 + 02:05 방향전환) 이후, 다음을 요청받음:
|
||||
|
||||
1. 드론 경로를 **실제 GPS 측정점(동그라미)+선**으로 표시 → 표시했더니 **먼 쪽에서 톱니로 튐**(영상은 매끄러운데).
|
||||
2. 드론 경로에 **투명도** 추가.
|
||||
3. **선형(중심선)** 과 **드론 궤적** 을 **각각 독립 on/off**.
|
||||
4. 카메라 파라미터를 제외한 **모든 표시 설정**을 **"화면표시 옵션" 별도 버튼 → 팝업**으로 분리.
|
||||
|
||||
---
|
||||
|
||||
## 1. 드론 경로 — 측정점 점+선 → 이동평균 평활
|
||||
|
||||
`client/src/components/overlay/StationOverlay.tsx` 캐시 빌드부.
|
||||
|
||||
### 문제
|
||||
GPS 노이즈(±0.5m) + 10Hz 계단이 **z=0(드론 58m 아래·지평선 쪽) 비스듬 투영**에서 화면상 큰 톱니로 증폭. 실제 비행/영상은 매끄러움.
|
||||
|
||||
### 해결
|
||||
경로점을 **±15프레임 이동평균(`sm`)** 으로 펴서 투영. 5프레임마다 서브샘플, 점(dot)+선 동시 생성.
|
||||
|
||||
```ts
|
||||
const BACK = 150, FWD = 1500, STEP = 5, PW = 15;
|
||||
const sm = (i) => { // ±PW 프레임 lat/lon 평균
|
||||
let la=0, lo2=0, n=0;
|
||||
for (let k=max(0,i-PW); k<=min(len-1,i+PW); k++){ la+=frames[k].lat; lo2+=frames[k].lon; n++; }
|
||||
return [la/n, lo2/n];
|
||||
};
|
||||
for (let i=lo; i<=hi; i+=STEP) {
|
||||
const [la,lo2] = sm(i);
|
||||
const cc = toCameraCoords(drone, la, lo2, Z, params, worldOrigin);
|
||||
if (prevCc) { const s=segFrom(prevCc,cc); if (s) dronePathSegs.push(s); }
|
||||
if (cc.Zc>=CLIP_Z) { const p=pixelFromCamera(cc,params); if(화면안) dronePathPts.push([p.pxRaw,p.pyRaw]); }
|
||||
prevCc = cc;
|
||||
}
|
||||
```
|
||||
|
||||
`RenderCache`에 `dronePathPts: [number,number][]` 추가.
|
||||
|
||||
---
|
||||
|
||||
## 2. 투명도 (dronePathAlpha)
|
||||
|
||||
상태/ref 추가, 기본 0.55. RAF에서 선·점 색의 alpha에 적용.
|
||||
|
||||
```ts
|
||||
const [dronePathAlpha, setDronePathAlpha] = useState(0.55);
|
||||
const dronePathAlphaRef = useRef(0.55);
|
||||
// RAF:
|
||||
const dpa = dronePathAlphaRef.current;
|
||||
ctx.strokeStyle = `rgba(80,200,255,${dpa})`; // 선
|
||||
ctx.fillStyle = `rgba(255,230,90,${dpa})`; // 점
|
||||
```
|
||||
|
||||
화면표시 옵션 팝업에 "투명도" 슬라이더(0.1~1.0).
|
||||
|
||||
---
|
||||
|
||||
## 3. 선형(중심선)·드론 궤적 독립 토글
|
||||
|
||||
기존: 중심선·드론경로·측점·POI 모두 `visible`(부모 측점선 토글)에 묶임 → 측점선 OFF면 전부 사라짐.
|
||||
|
||||
### 변경
|
||||
- `showCenterline` 상태/ref 신설(기본 true).
|
||||
- **캐시 빌드 게이트**: `!visible` → `!(visible || showCenterline || showDronePath)` (effect deps에 `showCenterline` 추가).
|
||||
- **RAF 그리기 분리**:
|
||||
```
|
||||
if (cache) {
|
||||
if (showCenterlineRef.current) → 중심선
|
||||
if (dronePathSegs/Pts) → 드론 경로 (showDronePath 시에만 캐시에 채워짐)
|
||||
if (visibleRef.current) → 측점/POI 라벨
|
||||
}
|
||||
```
|
||||
(기존 `if (visibleRef.current && cache)` 한 덩어리를 위 3개로 분해. 라벨 블록을 `if(visibleRef.current){…}`로 감싸고 닫는 중괄호 1개 추가.)
|
||||
|
||||
### 동작 변화(주의)
|
||||
부모의 **측점선 토글(visible)** 은 이제 **측점/POI 라벨만** 제어. **중심선(선형)은 화면표시 옵션의 "선형(중심선) 표시" 토글**(기본 ON)로 별도 제어. 드론 궤적도 측점선과 무관하게 단독 표시 가능.
|
||||
|
||||
---
|
||||
|
||||
## 4. 패널 분리 — 카메라 파라미터 / 화면표시 옵션
|
||||
|
||||
우측(나침반 아래) 버튼 2개:
|
||||
- **▼ 화면표시 옵션**(`showDisplay`): 표시 토글(선형/드론궤적+표고+투명도), 스무딩(smooth/EMA), POI 필터/높이(앞·옆·드론고도비교), POI 위치 편집(편집모드/표고/내보내기·가져오기/DEM/보정초기화).
|
||||
- **▼ 카메라 파라미터**(`showControls`): 자세 보정(Yaw/Pitch/Roll), 위치 보정(offX/Y/Z·지오이드), 내부표정(f/cx₀/cy₀/senW/senH), 실시간 readout, params 초기화.
|
||||
|
||||
분류 기준: **드론 카메라 투영 파라미터 = 카메라 파라미터**, 그 외 표시/필터/편집 = 화면표시 옵션.
|
||||
|
||||
`const [showDisplay, setShowDisplay] = useState(false);` 추가. 두 팝업은 독립 토글(동시 열림 가능, 각 `max-h-[80vh] overflow-y-auto`).
|
||||
|
||||
---
|
||||
|
||||
## 변경 파일
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
- 상태/ref: `showCenterline`, `dronePathAlpha`, `showDisplay`
|
||||
- `RenderCache.dronePathPts`
|
||||
- 캐시 빌드: 경로 평활(sm)+점 수집, 게이트 `(visible||showCenterline||showDronePath)`
|
||||
- RAF: 중심선/드론경로/라벨 독립 그리기 + 투명도
|
||||
- 패널 JSX: 1개 → 화면표시 옵션 + 카메라 파라미터 2팝업
|
||||
|
||||
빌드: `index-BbLxqCEp.js` ✓ (node20 `npm run build -w client`)
|
||||
|
||||
## 확인
|
||||
Ctrl+Shift+R → 우측 나침반 아래 버튼 2개. "화면표시 옵션"에서 선형/드론궤적 독립 토글·투명도·표고 조절. "카메라 파라미터"는 투영 보정만.
|
||||
@@ -0,0 +1,40 @@
|
||||
# 교량(신대천교(복)) 영상 오버레이 라벨 표출
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 증상
|
||||
영상에서 다리 위가 신대천교(복)인데 **신대천교(복) 라벨이 영상에 표시되지 않음**.
|
||||
|
||||
## 원인 (버그 아님 — 의도된 라우팅)
|
||||
- 신대천교(복)은 `building/03)교량.csv` 출신 **카테고리 `교량`** 구조물
|
||||
(`_POI.csv`/지장물엔 없음 — [2026-06-22_POI투영오차-개선.md](2026-06-22_POI투영오차-개선.md) §13 참조).
|
||||
- §15에서 구조물 표출 위치를 분리: **교량(03)·터널(04)=하단 스테이션바**, 구교(06)·지장물·_POI=영상.
|
||||
- `StationOverlay`의 영상 구조물 필터가 `category === '구교'`만 통과시켜 `교량`은 영상에서 제외됨.
|
||||
|
||||
## 조치 (사용자 결정: 영상은 라벨 모두 표출 / 시설등급 필터는 스테이션바 전용)
|
||||
`StationOverlay.tsx`의 영상 구조물 필터를 **모든 선로 구조물(교량/터널/구교)** 허용으로 변경:
|
||||
```diff
|
||||
+ const OVERLAY_STRUCT_CATEGORIES = new Set(['구교', '교량', '터널']);
|
||||
...
|
||||
- s.category === '구교' && typeof s.lat === 'number' && typeof s.lon === 'number'
|
||||
+ s.category != null && OVERLAY_STRUCT_CATEGORIES.has(s.category) &&
|
||||
+ typeof s.lat === 'number' && typeof s.lon === 'number'
|
||||
```
|
||||
- 교량/터널/구교 모두 기존 POI 파이프라인(평면 거리필터·title 보간·EMA·드래그/DEM 보정)으로 표출.
|
||||
- 역사(철도역, type='station')는 미포함 → 스테이션바 전용 유지.
|
||||
|
||||
### 시설등급(시설종별) 필터 — 영상 무관, 스테이션바 전용 (기존 구조 확인)
|
||||
- grade 필터는 `isGradeVisible(s.grade, gradeFilter)`로 **StationBar.tsx**·**RoutePanel.tsx에만** 적용.
|
||||
영상 오버레이는 grade를 보지 않음 → **영상은 시설등급과 무관하게 항상 모두 표출**(요구사항 그대로).
|
||||
- 따라서 코드 변경은 영상 필터(위)만으로 충분. 스테이션바 측은 미변경.
|
||||
|
||||
## 참고/한계
|
||||
- 영상 POI 거리필터 기본값(앞 60m/옆 40m)이 적용됨. 신대천교(복)이 멀리 있으면
|
||||
카메라 파라미터 패널의 **POI 앞쪽/옆쪽** 슬라이더를 키워야 보일 수 있음.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~95k / output ~6k tokens
|
||||
@@ -0,0 +1,47 @@
|
||||
# 화면표시 옵션 — "기본값" 버튼 + 설명문구 툴팁화
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 요구
|
||||
1. 화면표시 옵션 창 우측 하단에 **"기본값"** 버튼 → 누르면 옵션 일괄 복귀.
|
||||
2. 컨트롤 사이의 **설명문구**가 세로 공간을 차지 → **마우스 오버 툴팁**으로 바꿔 패널 높이 축소.
|
||||
|
||||
## 구현
|
||||
|
||||
### 1. 기본값(DISPLAY_DEFAULTS) + 버튼
|
||||
- 사용자 확정 기본값(스크린샷 기준)을 모듈 상수로 정의, **useState 초기값과 단일 출처 공유**:
|
||||
```ts
|
||||
const DISPLAY_DEFAULTS = {
|
||||
showCenterline: true, showDronePath: true,
|
||||
dronePathZ: 58, dronePathAlpha: 0.75,
|
||||
smoothHalf: 60, emaAlpha: 1.0,
|
||||
maxPoiFront: 310, maxPoiSide: 135,
|
||||
poiDroneHeight: true, droneHeightDrop: 24,
|
||||
};
|
||||
```
|
||||
(이전 코드 기본값 dronePathZ 78 / maxPoiFront 240 / maxPoiSide 80 → 스크린샷 값으로 갱신.)
|
||||
- `resetDisplayDefaults()` 콜백이 위 10개 상태를 일괄 set. POI 보정/편집모드는 제외(별도 "보정 초기화" 유지).
|
||||
- 패널 최하단에 카메라 패널의 "초기화"와 동일한 스타일의 **"기본값"** 버튼(우측 정렬) 추가.
|
||||
- 참고: localStorage 영속화 대상(maxPoiFront/Side·smoothHalf·emaAlpha·params)은 버튼 클릭 후 디바운스 저장으로 기본값이 유지됨. 나머지(dronePathZ 등)는 원래 세션 한정.
|
||||
|
||||
### 2. 설명문구 → 툴팁
|
||||
- `ParamRow`에 `tip?: string` 추가: 있으면 라벨에 **점선 밑줄 + 마우스오버 네이티브 툴팁**(`title`).
|
||||
- 별도 `InfoTip` 컴포넌트(작은 `ⓘ`, `title` 툴팁) 추가 → 섹션 헤더용.
|
||||
- 기존 `text-gray-600` 설명 div 6개 제거하고 해당 컨트롤/헤더의 tip으로 이전:
|
||||
| 기존 설명 div | 이전 위치 |
|
||||
|---|---|
|
||||
| GPS 측정점 표고/α | 경로표고·투명도 ParamRow tip |
|
||||
| ±fr=±ms | smooth ParamRow tip |
|
||||
| α→lag ms | EMA α ParamRow tip |
|
||||
| 진행방향 앞/옆 이내 표시 | POI 앞쪽·옆쪽 ParamRow tip |
|
||||
| POI 드론 아래 배치 | 이격거리 ParamRow tip |
|
||||
| 편집/DEM 안내 | "POI 위치 편집" 헤더 InfoTip |
|
||||
- 편집모드 안내문(긴 1줄)도 헤더 툴팁으로 이전, 상태줄은 `보정된 POI N개`/`드래그 중`만 표시.
|
||||
- 동적 수치(ms·lag·현재값)는 툴팁 문자열에 그대로 보간 → 정보 손실 없음.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 25분
|
||||
**Context 사용량**: input ~140k / output ~9k tokens
|
||||
@@ -0,0 +1,30 @@
|
||||
# POI 거리필터 — 앞/옆 분리 → 단일 표시 범위(직선거리)로 통합
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 요구
|
||||
"POI 앞쪽/옆쪽"을 **"POI 표시 범위"** 하나로 통합. 드론 위치와의 **직선거리**를 기준으로
|
||||
범위 안이면 표시, 밖이면 제외.
|
||||
|
||||
## 구현
|
||||
- 비등방 필터(`fwd > MAX_FRONT || |side| > MAX_SIDE`)를 **단일 반경 필터**로 교체:
|
||||
```ts
|
||||
if ((cc.distH ?? 0) > MAX_RANGE) continue; // distH = 드론↔POI 수평 직선거리(m, 고도차 무시)
|
||||
```
|
||||
`cc.distH`는 `toCameraCoords`가 항상 채우는 수평 평면 직선거리.
|
||||
- 상태 `maxPoiFront`/`maxPoiSide` → **`maxPoiRange`** 단일화(ref/effect/precompute 인자/호출부/의존성 일괄).
|
||||
- 패널 UI: 슬라이더 2개("POI 앞쪽"/"POI 옆쪽") → **1개 "POI 범위"**(10~1000m, 툴팁: "드론 위치와의 수평 직선거리 — 범위 안만 표시").
|
||||
- 기본값(`DISPLAY_DEFAULTS`): `maxPoiFront 310 / maxPoiSide 135` → **`maxPoiRange 300`**. "기본값" 버튼·초기 useState 공유.
|
||||
- 영속화(localStorage `ghivideo:calib`): `maxPoiRange` 저장. **구버전 마이그레이션** — 저장값에 `maxPoiRange`가 없고 `maxPoiFront`만 있으면 그 값으로 복원.
|
||||
|
||||
## 참고
|
||||
- 직선거리는 수평 평면거리(distH) 사용(기존 앞/옆도 평면 분해였음 → 일관). 드론 높이(~35m)는 무시되나
|
||||
100m+ 범위에선 3D 직선거리와 거의 동일.
|
||||
- 트레이드오프: 단일 반경이라 "앞으로 멀리 + 옆으로 좁게"는 더 이상 불가(사용자 요청대로 단순화).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~165k / output ~5k tokens
|
||||
@@ -0,0 +1,39 @@
|
||||
# 드론 회전 시 중심선·궤적선이 뒤늦게 따라오는 어긋남 — 적응형 평활
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 증상
|
||||
드론이 회전하고 나면 스테이션 선(중심선)과 드론 궤적선이 **뒤늦게 따라와** 회전 구간 중간에
|
||||
영상과 어긋나 보임.
|
||||
|
||||
## 원인
|
||||
선/궤적은 RAF에서 `poseAt → smoothFrame`의 **평활된 드론 자세**로 매 프레임 투영한다.
|
||||
`smoothHalf` 기본값이 **60프레임(±2초)** 중심이동평균이라, 회전이 끝난 직후에도 평균창의
|
||||
뒷부분(trailing)에 회전 전 낮은 yaw 프레임들이 최대 ~2초간 포함됨 → **오버레이 yaw가
|
||||
실제보다 지연**되어 선이 뒤처졌다가 천천히 따라잡음. 영상은 평활 안 된 실제 자세라 어긋남.
|
||||
(라벨은 점이라 덜 띄지만 긴 선은 각도 오차가 크게 보임.)
|
||||
|
||||
## 조치 — `smoothFrame` 적응형 창
|
||||
국부 yaw 변화율로 평균창을 가변:
|
||||
```ts
|
||||
// ±k(=3) 프레임 yaw 변화율(deg/frame, 최단각)
|
||||
const rate = |Δyaw| / (2k);
|
||||
// rate=0 → 풀창 유지, 회전 클수록 축소(하한 5프레임). 게인 8: 1°/fr ≈ halfWin/9.
|
||||
effHalf = Math.max(5, Math.round(halfWin / (1 + 8 * rate)));
|
||||
```
|
||||
- **회전 중**: 창이 좁아져 실제 자세를 즉시 추종 → 지연/어긋남 제거.
|
||||
- **직선(저변화율)**: 풀창(±smoothHalf) 유지 → GPS/자세 노이즈는 그대로 억제.
|
||||
- 모든 소비처(선·궤적 `poseAt`, 라벨 precompute, 나침반/캐시)가 같은 `smoothFrame`을 쓰므로
|
||||
일괄 적용. `smooth` 슬라이더는 이제 **최댓값**(직선 기준) 의미 → 툴팁에 "회전 중 자동 축소" 명시.
|
||||
|
||||
## 참고
|
||||
- 적응형이라 `halfWin > 6`일 때만 동작(작은 값은 그대로). yaw는 raw 프레임 배열에서 산출.
|
||||
- 위치(GPS) 평활도 함께 좁아져 회전 시 궤적 코너 라운딩도 완화됨.
|
||||
- 더 민감/둔감 조절은 게인(8)·하한(5)·k(3) 상수로. 떨림이 재발하면 게인↓.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~185k / output ~5k tokens
|
||||
@@ -0,0 +1,39 @@
|
||||
# 회전 종료 후 중심선·궤적선이 한 번 튀는 현상 — 에지보존 평활로 교체
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**선행**: [2026-06-24_0957_회전시-선오버레이-지연어긋남-적응형평활.md](2026-06-24_0957_회전시-선오버레이-지연어긋남-적응형평활.md)
|
||||
|
||||
## 증상
|
||||
드론 회전이 끝난 직후 스테이션선·드론궤적선이 **한 번 튐**.
|
||||
|
||||
## 원인
|
||||
직전 작업에서 넣은 **변화율 기반 적응형 창**(`effHalf = halfWin/(1+8·rate)`)의 부작용.
|
||||
회전이 끝나 yaw 변화율이 급감하면 `effHalf`가 **한꺼번에 5~7 → 60프레임으로 재확장**됨.
|
||||
그 순간 trailing window에 회전 프레임들이 다시 포함되어 평균 yaw가 급변 → 1회 튐.
|
||||
|
||||
## 조치 — 에지(회전 경계) 보존 적응형 창
|
||||
변화율로 창 크기를 정하지 않고, **중심 yaw 와 차이가 임계 이내인 인접 프레임까지만** 확장:
|
||||
```ts
|
||||
const YAW_EDGE = 8; // deg
|
||||
const near = idx => |최단각(frames[idx].yaw - frames[i].yaw)| <= YAW_EDGE;
|
||||
let lo = i, hi = i;
|
||||
while (lo > i-halfWin && near(lo-1)) lo--; // 회전 프레임 만나면 정지
|
||||
while (hi < i+halfWin && near(hi+1)) hi++;
|
||||
```
|
||||
- 회전 구간이 직선 평균에 **섞이지 않음**(시간축 양방향 에지보존 = bilateral).
|
||||
- 회전 중: 창이 자연히 좁아 실제 자세 즉시 추종(지연 없음).
|
||||
- 회전 종료 후: 회전 프레임을 배제한 채 창이 **한 프레임씩** 부드럽게 재확장 → 튐 없음.
|
||||
- 직선: yaw 거의 일정 → 풀창(±halfWin)으로 노이즈 억제(기존 효과 유지).
|
||||
|
||||
## 튜닝
|
||||
- `YAW_EDGE`(8°): 작게 → 회전 추종↑·직선 노이즈억제↓, 크게 → 반대. GPS yaw 지터(보통 <8°)는
|
||||
창 확장을 막지 않도록 8°로 설정.
|
||||
- 단일 프레임 yaw 스파이크가 8° 초과하면 그 지점에서 창이 끊길 수 있으나, 창 경계가
|
||||
중심 이동에 따라 1프레임씩만 변하므로 **튐은 발생하지 않음**(완만).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~200k / output ~4k tokens
|
||||
@@ -0,0 +1,41 @@
|
||||
# 재생 직후 라벨이 몇 초 뒤 나타나는 지연 — 증분 게시 + 현재구간 우선 계산
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 증상
|
||||
재생을 누르고 **몇 초 뒤에야** 화면 라벨(측점/POI)이 나타남. 재생 즉시 보이길 원함.
|
||||
|
||||
## 원인
|
||||
`startLabelPrecompute`가 전 프레임 라벨을 청크(200프레임)로 사전계산하는데,
|
||||
완성된 `newMap`을 **모든 프레임 계산이 끝난 뒤에야** `labelMapRef.current`에 1회 게시.
|
||||
→ 긴 영상은 전체 사전계산(수 초)이 끝날 때까지 RAF가 빈 맵을 조회 → 라벨 미표시.
|
||||
또 항상 idx 0부터 계산해, 중간에서 재생(seek 후)하면 그 구간이 더 늦게 준비됨.
|
||||
|
||||
## 조치
|
||||
1. **현재 재생 프레임부터 순환 계산**: `startIdx = currentFrameIdxRef.current` 에서 시작해
|
||||
`(startIdx + processed) % total` 순서로 처리 → 화면에 보이는 구간을 가장 먼저 계산.
|
||||
2. **첫 청크 후 즉시 게시**(초기 로드 한정): 맵이 비어 있으면(`labelMapRef.current.size===0`)
|
||||
첫 200프레임(현재 구간)을 채우자마자 `labelMapRef.current = newMap` 게시 → 재생 즉시 라벨.
|
||||
이후 같은 Map 에 **추가만**(삭제 없음) 하므로 나머지 구간이 깜빡임 없이 보강됨.
|
||||
3. **재계산(파라미터 변경)은 기존 방식 유지**: 기존 맵이 있으면 완료 시 한 번에 교체
|
||||
(`livePublish=false`) → 슬라이더 조정 중 기존 라벨이 사라지지 않음(깜빡임 방지).
|
||||
|
||||
```ts
|
||||
const livePublish = labelMapRef.current.size === 0; // 초기 로드 여부
|
||||
// 청크 후:
|
||||
if (livePublish && !published) { labelMapRef.current = newMap; published = true; }
|
||||
// 완료 시:
|
||||
if (!livePublish) labelMapRef.current = newMap;
|
||||
```
|
||||
|
||||
## 효과
|
||||
- 초기 로드/재생: 첫 청크(≈수~수십 ms 계산, idle timeout 200ms 상한) 후 바로 라벨 표시.
|
||||
- seek 후 재생: 그 위치부터 계산되어 즉시 표시.
|
||||
- 파라미터 재계산: 종전처럼 완성 후 일괄 교체(무깜빡임).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~215k / output ~5k tokens
|
||||
@@ -0,0 +1,42 @@
|
||||
# 라벨이 영상보다 먼저 뜨는 현상 — 영상 첫 프레임 표시 후로 게이트
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/store/playerStore.ts`, `client/src/hooks/useVideoPlayer.ts`,
|
||||
`client/src/components/player/VideoPlayer.tsx`, `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1019_라벨-재생즉시표시-증분게시.md](2026-06-24_1019_라벨-재생즉시표시-증분게시.md)
|
||||
|
||||
## 증상
|
||||
재생 시 라벨이 **영상보다 살짝 먼저** 표시됨. (직전 작업으로 라벨이 즉시 게시되면서,
|
||||
영상 첫 프레임이 디코드·표시되기 전 검은 화면 위에 라벨이 먼저 보임.)
|
||||
|
||||
## 원인
|
||||
StationOverlay RAF는 데이터/캐시만 준비되면 영상 표시 여부와 무관하게 캔버스에 그림.
|
||||
영상 `<video>`의 첫 프레임 표시 시점과 동기화하는 신호가 없었음.
|
||||
|
||||
## 조치 — "영상 첫 프레임 표시 가능" 신호로 오버레이 게이트
|
||||
1. **playerStore**: `videoReady: boolean`(기본 false) + `setVideoReady` 추가.
|
||||
`setSource` 시 false 로 리셋(영상 교체 때 다시 게이트).
|
||||
2. **useVideoPlayer**: Video.js 이벤트 연결
|
||||
- `loadstart` → `setVideoReady(false)` (새 소스 로드 시작)
|
||||
- `loadeddata` → `setVideoReady(true)` (첫 프레임 표시 가능)
|
||||
- `playing` → `setVideoReady(true)` (폴백)
|
||||
3. **VideoPlayer**: store `videoReady` 를 StationOverlay 에 prop 전달.
|
||||
4. **StationOverlay**: `videoReady` prop(기본 true=하위호환) → `videoReadyRef`. RAF draw 에서
|
||||
`clearRect` 직후 **`if (!videoReadyRef.current) return;`** → 첫 프레임 전엔 라벨/선/나침반 미표시.
|
||||
|
||||
```ts
|
||||
// StationOverlay draw()
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
if (!videoReadyRef.current) return; // 영상 첫 프레임 전 오버레이 차단
|
||||
```
|
||||
|
||||
## 효과
|
||||
- 영상 첫 프레임이 표시된 뒤에야 오버레이가 그려짐 → 라벨이 먼저 뜨는 깜빡임 제거.
|
||||
- 영상 교체(폴더 재로드) 시 `loadstart` 로 다시 게이트되어 새 영상 첫 프레임까지 대기.
|
||||
- prop 기본값 true 라 StationOverlay 단독 사용처(있다면)는 기존대로 동작.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~235k / output ~5k tokens
|
||||
@@ -0,0 +1,33 @@
|
||||
# 히스토리 파일명에 시간(HHMM) 추가 + POI 라벨 원근 현상 분석
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `CLAUDE.md`, `docs/history/*` (파일명 변경)
|
||||
|
||||
## 1) 히스토리 파일명 규칙: 날짜 → 날짜+시간
|
||||
요구: 각 작업 파일명에 날짜와 **시간**을 함께 기록.
|
||||
- 규칙 변경: `docs/history/YYYY-MM-DD_{작업명}.md` → **`YYYY-MM-DD_HHMM_{작업명}.md`** (CLAUDE.md 갱신).
|
||||
- 오늘 작성한 7개 파일을 작성 시각(mtime) 기준으로 일괄 리네임(0926~1026), 파일 간 상호 링크(선행/관련) 갱신.
|
||||
|
||||
## 2) "멀리선 뒤로 밀리고 가까이선 빨리 아래로 빠짐" — 원근 현상(버그 아님)
|
||||
사용자 관찰: 라벨이 멀 땐 살짝씩 밀리고, 바로 밑을 지날 땐 드론 이동보다 빨리 아래로 빠져 사라짐.
|
||||
|
||||
### 결론(정정)
|
||||
이는 **정상적인 원근 투영 효과**다. 수직간격 Δh 일정·드론고도 일정이어도 화면 각속도는
|
||||
`dβ/dt = v·Δh/(D²+Δh²)` — 멀리(D 큼) 느리고, 천정점(D→0) 부근 `v/Δh`로 최대. 즉 머리 위를
|
||||
지나가는 고정 지물은 원래 그렇게 보인다. (앞서 "far/near 부호 뒤집힘" 진단은 과했음 — 정정.)
|
||||
|
||||
### 진짜 잔차의 판별
|
||||
라벨이 **영상 속 실제 대상물에 붙어 있으면 정상**(고치면 오히려 틀림). 대상에서 **벗어나면**
|
||||
'드론 24m 아래 상수' 가정이 그 지점 실제 간격과 달라서 생기는 잔차:
|
||||
- 가까이서 라벨이 대상보다 먼저 빠짐 → 실제 간격 < 24(과대) → `이격거리`↓ 또는 지형표고 사용.
|
||||
- 멀리서 라벨이 대상보다 떠 보임 → 24 과소 → 반대 조정.
|
||||
- 지형표고(측점 z/DEM)는 구간별로 변하므로 상수 가정의 잔차를 근본 제거(POI투영오차 문서 §18·§19).
|
||||
|
||||
### 조치
|
||||
- 사용자에게 "라벨이 실제 지물에 붙는지/벗어나는지" 확인 요청. 코드 변경은 보류(현상 정상).
|
||||
|
||||
## 검증
|
||||
- 코드 변경 없음(문서/파일명만). 빌드 불요.
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~255k / output ~6k tokens
|
||||
@@ -0,0 +1,31 @@
|
||||
# 라벨이 멀리선 뒤로/가까이선 먼저 — 세로 화각(focal·sensorH) 보정 진단
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: 없음 (진단/가이드)
|
||||
|
||||
## 증상 (사용자 확인)
|
||||
라벨이 지물에 정확히 붙지 않음. **멀리 있을 때 영상 위치보다 뒤(위)로 조금씩 밀리고,
|
||||
가까이 있을 때 영상보다 먼저(아래로) 지나감.** → 화면 중심 기준 위/아래가 **반대 방향**으로 벌어짐.
|
||||
|
||||
## 진단: z/이격거리/pitch 아님 → focal·sensorH(세로 FOV)
|
||||
- z·이격거리 오차: 위아래가 **같은 방향**으로 밀림(한쪽으로). 교차 안 생김 → 배제.
|
||||
- pitch 오차: 전체가 같은 화면방향으로 시프트. 교차 안 생김 → 배제.
|
||||
- **focal/sensorH(세로 배율) 오차**: `py = 0.5 + cy0 + (Yc/Zc)·(f/sensorH)`.
|
||||
카메라 −30° 틸트 → 먼 POI는 화면 중심 위, 가까운 POI는 중심 아래.
|
||||
`f/sensorH`가 과대 → 중심에서 멀수록 과확대 → 위(먼 것)는 더 위로, 아래(가까운 것)는 더 아래로
|
||||
= **멀리 뒤로 / 가까이 먼저**. 사용자 증상과 정확히 일치.
|
||||
|
||||
## 가이드 (코드 변경 없음 — 패널 튜닝)
|
||||
카메라 파라미터 패널에서 POI가 멀→근 통과하는 구간을 보며:
|
||||
- `f`(초점)를 낮추거나 `sen H`를 키워 세로 배율↓ → 위/아래가 지물로 수렴.
|
||||
- 반대로 안쪽으로 과수렴하면 `f`↑.
|
||||
- 값은 `ghivideo:calib:<baseName>` 로 자동 저장.
|
||||
|
||||
현재 기본 `f=24 / sen W=36 / sen H=20.25`(16:9 가정). 실제 4K 세로 화각이 다르면 본 증상 발생.
|
||||
사용자가 패널에서 맞춘 `f`/`sen H` 값을 알려주면 **기본값(DEFAULT_CAMERA_PARAMS)으로 반영** 예정.
|
||||
|
||||
## 다음 단계
|
||||
- 사용자 튜닝값 수령 시 `geoProjection.ts` 기본 focal/sensorH 갱신.
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~275k / output ~5k tokens
|
||||
@@ -0,0 +1,45 @@
|
||||
# 오버레이가 영상과 어긋남 — object-fit:cover 크롭 정렬 (영상 해상도 사용)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/store/playerStore.ts`, `client/src/hooks/useVideoPlayer.ts`,
|
||||
`client/src/components/player/VideoPlayer.tsx`, `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1051_라벨-멀리뒤로-가까이먼저-FOV진단.md](2026-06-24_1051_라벨-멀리뒤로-가까이먼저-FOV진단.md)
|
||||
|
||||
## 발단
|
||||
"영상이 재생되는 브라우저의 해상도를 알 수 있나?" → 영상 표시 방식 점검 중 근본 원인 발견.
|
||||
|
||||
## 원인 (핵심)
|
||||
영상은 `index.css`에서 **`object-fit: cover`** 로 표시 → 컨테이너 비율 ≠ 영상 비율(3840×2160=16:9)
|
||||
이면 영상이 **확대·크롭**되어 채워짐. 그런데 **오버레이 캔버스는 컨테이너 전체**에 정규좌표(0~1)를
|
||||
매핑하고 있었음. 즉 영상은 크롭된 영역에 보이는데 오버레이는 컨테이너 기준 → **가로/세로 배율·오프셋
|
||||
불일치**. 이것이 라벨/선이 영상과 어긋나(멀리·가까이 다르게) 보이던 핵심 원인.
|
||||
|
||||
## 조치 — 오버레이를 영상 cover 사각형에 정렬 (영상 원본 해상도 사용)
|
||||
1. **영상 해상도 확보**: playerStore `videoWidth/videoHeight` + `setVideoSize`. useVideoPlayer 에서
|
||||
`loadedmetadata`/`loadeddata` 시 `player.videoWidth()/videoHeight()` 저장. VideoPlayer→StationOverlay prop 전달.
|
||||
2. **cover 변환**: RAF draw 에서 영상 해상도로 cover 사각형 계산
|
||||
```ts
|
||||
const s = Math.max(W/vW, H/vH); // cover 배율(넘침=크롭)
|
||||
dispW=vW*s; dispH=vH*s; offX=(W-dispW)/2; offY=(H-dispH)/2;
|
||||
const vx = nx => offX + nx*dispW; // 정규(영상프레임) → 화면 px
|
||||
const vy = ny => offY + ny*dispH;
|
||||
```
|
||||
중심선/드론궤적/측점·POI 라벨/드래그 피드백의 좌표를 모두 `*W,*H` → `vx(),vy()` 로 교체.
|
||||
(나침반 HUD 는 화면 코너 고정이라 컨테이너 W/H 유지.)
|
||||
3. **포인터 역변환**: `clientToVideoNorm()` 추가 — 클릭 client px → 영상프레임 정규좌표(cover 역변환).
|
||||
POI 드래그 hit-test/이동이 크롭 상태에서도 정확히 일치.
|
||||
4. **해상도 표시**: 카메라 파라미터 패널 진단줄에 `영상 W×H (비율 · cover 정렬)` 노출(사용자 확인용).
|
||||
|
||||
## 효과
|
||||
- 컨테이너 비율이 16:9가 아니어도 오버레이가 **크롭된 영상과 픽셀 단위로 정렬**됨.
|
||||
- 영상 해상도 미확정(vW/vH=0) 시 컨테이너 기준 폴백(기존 동작).
|
||||
- 앞서 'FOV 의심'(§1051) 잔차의 상당부분이 실은 이 cover 불일치였을 가능성 큼 → 적용 후 재확인 필요.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
## 다음 단계
|
||||
- 적용 후에도 멀·근 잔차가 남으면 그때 focal/sensorH(세로 FOV) 미세조정(§1051).
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~300k / output ~9k tokens
|
||||
@@ -0,0 +1,47 @@
|
||||
# 1-클릭 FOV(초점) 보정 — POI를 실제 위치로 드래그해 focal 역산
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1051_라벨-멀리뒤로-가까이먼저-FOV진단.md](2026-06-24_1051_라벨-멀리뒤로-가까이먼저-FOV진단.md),
|
||||
[2026-06-24_1106_오버레이-objectfit-cover정렬.md](2026-06-24_1106_오버레이-objectfit-cover정렬.md)
|
||||
|
||||
## 배경
|
||||
cover 정렬 후에도 "멀리 뒤로 / 가까이 먼저" 잔차 동일 → 투영 수식 버그 아님(검증). 남은 원인은
|
||||
**FOV(초점 f) 미스매치**(중심 기준 배율 오차 → 위/아래 반대로 벌어짐). 손으로 f 맞추는 대신
|
||||
**1-클릭 보정** 추가.
|
||||
|
||||
## 기능
|
||||
편집 모드와 별개의 **🎯 FOV(초점) 보정** 모드. POI 라벨 하나를 영상 속 **실제 위치로 드래그**하면,
|
||||
그 대응점으로 focal 을 역산해 즉시 적용 → 전 거리(멀·근) 정렬.
|
||||
|
||||
### 역산 수식
|
||||
```
|
||||
px = 0.5 + cx0 + (Xc/Zc)·(f/sW), py = 0.5 + cy0 + (Yc/Zc)·(f/sH)
|
||||
a=Xc/Zc, b=Yc/Zc, u=px*-0.5-cx0, v=py*-0.5-cy0
|
||||
f = ((a/sW)·u + (b/sH)·v) / ((a/sW)² + (b/sH)²) // 두 축 공통 f 의 최소제곱
|
||||
```
|
||||
- f 는 가로·세로 공통 → 한 점이면 화각 배율 결정(센서·픽셀 정방, 16:9 가정).
|
||||
- POI 카메라좌표는 렌더와 동일한 poiZ(드론높이 모드면 드론−이격거리, 아니면 선택 z)로 계산.
|
||||
- `setParam('focalLen', clamp(10..100))` → 카메라 파라미터 패널에 반영·데이터셋별 영속화.
|
||||
|
||||
## 구현
|
||||
- 상태 `fovMode`(+ref), `poiDroneHeightRef`/`droneHeightDropRef`(역산용).
|
||||
- `onPoiPointerDown`: editMode **또는** fovMode 에서 POI 선택. 캔버스 pointer-events 도 동일 조건.
|
||||
- `onPoiPointerUp`: fovMode 면 focal 역산 분기(위 수식), 아니면 기존 위치(lat/lon) 보정.
|
||||
- 드래그 피드백 마커: editMode/fovMode 모두 표시.
|
||||
- UI: "🎯 FOV(초점) 보정" 토글(편집 모드와 상호 배타) + 안내 InfoTip + 현재 f 표시.
|
||||
|
||||
## 사용법
|
||||
1. 화면표시 옵션 → **🎯 FOV(초점) 보정** 클릭.
|
||||
2. 멀거나 가까운 POI 하나를 **영상 속 실제 위치로 드래그**(일시정지 권장).
|
||||
3. f 자동보정 → 멀·근 라벨이 함께 지물에 정렬. 값은 자동 저장.
|
||||
|
||||
## 한계
|
||||
- z(표고)가 크게 틀리면 세로 대응에 z 오차가 섞일 수 있음 → 먼저 DEM/표고 보정 후 FOV 보정 권장.
|
||||
- 한 점 기준이라, 가능하면 화면 중심에서 충분히 떨어진(위/아래) POI 로 보정하면 안정적.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 18분
|
||||
**Context 사용량**: input ~330k / output ~9k tokens
|
||||
@@ -0,0 +1,35 @@
|
||||
# FOV 보정을 세로화각(sensorH) 전용으로 전환 — 가로(중심선) 안 틀어지게
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1117_1클릭-FOV초점-보정.md](2026-06-24_1117_1클릭-FOV초점-보정.md)
|
||||
|
||||
## 결정적 단서 (사용자)
|
||||
- 카메라 패널에 **영상 해상도 정상 표시** → cover 정렬 동작 중(가로/세로 매핑 OK).
|
||||
- 현재 **f = 24.0mm**.
|
||||
- **f 보정 시 빨간 중심선(스테이션선)이 좌우로 벗어남.**
|
||||
|
||||
## 해석
|
||||
중심선이 가로로 잘 맞고 있다 = 가로(f/sW)는 정확. f 는 가로·세로 공통 배율이라 세로를
|
||||
맞추려고 f 를 바꾸면 **멀쩡한 가로가 틀어짐**. 따라서 잔차는 **세로 화각만 어긋남 → sensorH**.
|
||||
|
||||
## 조치 — FOV 보정을 sensorH 역산으로 교체
|
||||
드롭점의 세로(py)만 맞추고 가로(f/sW)는 그대로:
|
||||
```
|
||||
py = 0.5 + cy0 + (Yc/Zc)·(f/sH) → sH = (Yc/Zc)·f / (py* − 0.5 − cy0)
|
||||
```
|
||||
- f·sensorW(가로) 미변경 → 중심선 좌우 위치 불변.
|
||||
- sensorH 만 갱신(clamp 6~36, 슬라이더 범위) → 상하 벌어짐만 교정.
|
||||
- 세로 중심 근처(|py−0.5−cy0|<0.03)면 역산 불안정 → 무시 + 안내(위/아래 떨어진 POI 사용).
|
||||
- UI: "🎯 세로화각 보정", 현재 senH 표시, 안내 InfoTip 갱신.
|
||||
|
||||
## 사용법
|
||||
1. 🎯 **세로화각 보정** 모드 ON.
|
||||
2. **세로 중심에서 위(먼 POI)/아래(가까운 POI)로 충분히 떨어진** POI 하나를 영상 속 실제 위치로 드래그.
|
||||
3. sensorH 자동보정 → 상하 정렬, 빨간 중심선 좌우는 그대로. 데이터셋별 저장.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~360k / output ~5k tokens
|
||||
@@ -0,0 +1,35 @@
|
||||
# POI 높이를 지면고도 기준으로 — 고정 이격거리 → 기본 OFF (드론 고도 변동 대응)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 요구
|
||||
드론 고도가 오르내리는데, POI 이격거리를 고정값(24m)으로 두지 말고
|
||||
**드론고도 − 지면고도**로 POI 높이를 표시하고 싶다.
|
||||
|
||||
## 해석
|
||||
투영 수직간격 `gap = 드론고도(alt) − (POI 지면고도 z + 지오이드)`.
|
||||
- 고정 이격거리(ON): `gap=24m` 상수 → POI가 드론에 매달려 고도 변동 시 같이 떠/내려감(지물 이탈).
|
||||
- **지면고도 기준(OFF)**: POI를 지면고도 z 에 고정 → `gap`이 매 프레임 `드론고도−지면고도`로
|
||||
자동 계산 → 드론이 오르내려도 POI가 고정 지상점에 그대로 붙음. (= 사용자가 원한 동작)
|
||||
|
||||
즉 별도 신규 모드 불필요 — 기존 `poiDroneHeight=false`(지면고도 모드)가 정확히 이 계산.
|
||||
|
||||
## 조치
|
||||
- `DISPLAY_DEFAULTS.poiDroneHeight` **true → false** (지면고도 기준을 기본값으로).
|
||||
드론 고도 변동 환경에서 고정 이격거리는 부정확하므로 기본 off.
|
||||
- `poiDroneHeightRef` 초기값도 동일 상수 참조 → 자동 반영. 토글은 유지(원하면 고정모드로 복귀).
|
||||
- 지면고도 z 출처(false 모드): POI override(DEM/드래그) → 없으면 `nearestCL.z`(최근접 측점 표고).
|
||||
· 선로변 POI·교량(신대천교 등)은 측점 표고가 곧 지면/교량고도라 양호.
|
||||
· 선로에서 먼 POI는 **🌐 DEM 표고 자동적용**으로 실제 지면고도 채우는 걸 권장.
|
||||
|
||||
## 사용
|
||||
- 기본값이 지면고도 기준으로 시작. 멀리·가까이 모두 고정 지상점에 붙어 자연스럽게 추종.
|
||||
- 정밀이 필요하면 편집 모드 → 🌐 DEM 표고 자동적용(또는 표고 슬라이더/드래그).
|
||||
- 고정 이격거리로 되돌리려면 "드론 높이와 POI 이격거리" 토글 ON.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 7분
|
||||
**Context 사용량**: input ~385k / output ~4k tokens
|
||||
@@ -0,0 +1,40 @@
|
||||
# 라벨 클릭 → 속성 팝업 (라벨 정보 모드)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 요구
|
||||
영상의 라벨을 클릭하면 속성 팝업이 열리게.
|
||||
|
||||
## 구현 — "ℹ️ 라벨 정보" 모드
|
||||
편집/세로화각 보정과 상호 배타인 토글 모드. 켜면 캔버스가 클릭을 받고, 라벨을 누르면 속성 팝업 표시.
|
||||
|
||||
- 상태 `infoMode`(+ref) + `infoPopup`(클릭한 라벨 속성·화면좌표). 모드 끄면 팝업 자동 닫힘.
|
||||
- `onPoiPointerDown` 에 info 분기: 클릭점을 영상정규좌표로 변환(`clientToVideoNorm`) 후
|
||||
표시 중인 POI(`displayedPoiRef`)·측점(`displayedStRef`) 중 최근접(반경 ~0.04) 히트테스트.
|
||||
- 데이터 조회: POI=`allPoisRef`+`allStructuresRef`, 측점=`allGeoStationsRef`.
|
||||
- 표고 z = override(DEM/드래그) → 없으면 최근접 측점표고(`nearestCL`) → obj.z.
|
||||
- 드론 수평거리 = `toCameraCoords(...).distH`.
|
||||
- 빈 곳 클릭 시 팝업 닫힘.
|
||||
- 캔버스 `pointer-events`/커서: editMode·fovMode(이동) 또는 **infoMode(pointer)** 일 때 활성.
|
||||
- 팝업 UI: 제목(이모지)·종류·카테고리·위도·경도·표고(보정 표시)·드론 거리. ✕로 닫기.
|
||||
위치는 클릭한 라벨 화면좌표 옆(화면 밖으로 안 나가게 클램프).
|
||||
|
||||
## 표시 속성
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 제목 | cleanTitle (괄호 상/하 제거) |
|
||||
| 종류·카테고리 | POI/측점 · 교량/터널/구교/지장물/측점 등 |
|
||||
| 위도·경도 | 6자리 |
|
||||
| 표고 | groundZ(m), override면 "(보정)" |
|
||||
| 거리 | 드론 기준 수평거리(distH, m) |
|
||||
|
||||
## 참고
|
||||
- 라벨이 보일 때(측점선 표시 ON)만 클릭 가능(숨김 상태면 히트 대상 없음).
|
||||
- 팝업은 클릭 시점 위치에 고정(재생 중 라벨은 움직여도 팝업은 정보창으로 유지).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 14분
|
||||
**Context 사용량**: input ~410k / output ~7k tokens
|
||||
@@ -0,0 +1,38 @@
|
||||
# 라벨 정보 — 모드 버튼 제거, 호버 시 손커서·항상 클릭 가능
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1150_라벨정보-클릭팝업.md](2026-06-24_1150_라벨정보-클릭팝업.md)
|
||||
|
||||
## 요구
|
||||
- 라벨 위 → 손모양 커서, 라벨 밖 → 화살표 커서.
|
||||
- "라벨 정보 모드" 버튼 클릭 없이 항상 동작.
|
||||
|
||||
## 핵심 — 라벨 위에서만 캔버스가 입력을 받게 동적 토글
|
||||
오버레이 캔버스가 항상 입력을 먹으면 영상 클릭(재생/일시정지)이 막힌다. 그래서:
|
||||
- 부모(플레이어 컨테이너)에 `mousemove` 리스너 → 매 이동마다 라벨 히트테스트.
|
||||
- **라벨 위**: `canvas.pointerEvents='auto'` → 캔버스가 받음 → 커서 `pointer`(손), 클릭 시 팝업.
|
||||
- **라벨 밖**: `canvas.pointerEvents='none'` → 영상이 받음 → 커서 화살표, 클릭이 영상으로 통과.
|
||||
- 부모 `pointerdown`: target≠canvas(라벨 밖)면 팝업 닫기. 라벨 클릭(target=canvas)은 유지.
|
||||
- 팝업 자신은 `onPointerDown stopPropagation` → 팝업 내부 클릭으로 안 닫힘.
|
||||
- 편집/세로화각 보정 모드는 종전대로 캔버스 항상 입력(드래그) + 커서 `move`(이 모드 진입 시 팝업 닫음).
|
||||
|
||||
## 변경
|
||||
- `infoMode` 상태/토글 버튼 **제거** → 라벨 정보 항상 활성. 패널엔 안내문만.
|
||||
- `hitTestLabel(nx,ny)` 헬퍼 추출(POI+측점 최근접) — 클릭/호버 공용.
|
||||
- `onPoiPointerDown` 재구성: 편집/보정 모드면 드래그, 아니면 라벨 팝업.
|
||||
- 캔버스 className 에서 pointer-events/cursor 제거 → 새 useEffect 가 모드별로 `style` 직접 관리.
|
||||
- 편집/FOV 버튼의 상호배타에서 infoMode 제거.
|
||||
|
||||
## 동작 요약
|
||||
| 상황 | 커서 | 클릭 |
|
||||
|---|---|---|
|
||||
| 라벨 위(기본 모드) | 손(pointer) | 속성 팝업 |
|
||||
| 라벨 밖(기본 모드) | 화살표 | 영상 재생/일시정지 |
|
||||
| 편집/세로화각 모드 | 이동(move) | POI 드래그 |
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~440k / output ~7k tokens
|
||||
@@ -0,0 +1,47 @@
|
||||
# 라벨 팝업 — 다중 유지 + 라벨 추적 이동 + 빈클릭(팝업닫기/재생토글)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`, `client/src/components/player/VideoPlayer.tsx`
|
||||
**관련**: [2026-06-24_1159_라벨정보-항상활성-호버커서.md](2026-06-24_1159_라벨정보-항상활성-호버커서.md)
|
||||
|
||||
## 요구 (4가지)
|
||||
1. 라벨 클릭 시 기존 팝업 닫지 말고 계속 표시(다중).
|
||||
2. 팝업 있을 때 화면 클릭 → 팝업 닫힘.
|
||||
3. 팝업 없을 때 화면 클릭 → 재생/정지.
|
||||
4. 팝업이 라벨처럼 영상 재생에 따라 이동.
|
||||
|
||||
## 구현
|
||||
### 다중 팝업 + 라벨 추적(1·4)
|
||||
- `infoPopup`(단일) → `infoPopups: InfoPopup[]`(id=`${kind}:${title}`). 라벨 클릭 시 중복 id 아니면 append.
|
||||
- 각 팝업 DOM 에 ref 등록(`popupElsRef`). **RAF draw 에서** 해당 라벨의 현재 표시좌표(`displayedPoiRef`/
|
||||
`displayedStRef`)로 `el.style.left/top` 매 프레임 갱신 → 재생 중 라벨 따라 이동. 라벨이 화면에서
|
||||
사라지면 팝업 `visibility:hidden`, 재등장 시 표시.
|
||||
- ✕ 로 개별 닫기.
|
||||
|
||||
### 클릭 분기(2·3)
|
||||
- Video.js `controls:false` → 영상이 클릭을 처리 안 함. **캔버스가 모든 클릭을 받아** 직접 분기:
|
||||
- 라벨 클릭 → 팝업 추가.
|
||||
- 빈 곳 클릭 → 팝업 있으면 모두 닫기(`setInfoPopups([])`), 없으면 `onTogglePlay()`(재생/정지).
|
||||
- `onTogglePlay` prop 신설 → VideoPlayer 의 `handleTogglePlay` 연결. `hasPopupRef`로 팝업 유무 판별.
|
||||
- 캔버스 커서: 라벨 위 `pointer`(손) / 그 외 `default`(화살표) / 편집·보정 `move`.
|
||||
|
||||
### 입력 처리 단순화
|
||||
- 캔버스 `pointer-events` 를 항상 `auto`(편집/보정/기본 모두)로 통일 — 이전의 '라벨 위에서만 auto'
|
||||
토글 제거. 영상은 어차피 클릭 처리 안 하므로 캔버스가 전담. 패널(z-30)·스테이션바(z-20 후순위)는
|
||||
캔버스 위라 각자 클릭 정상.
|
||||
|
||||
## 동작표
|
||||
| 클릭 위치 | 팝업 유무 | 결과 |
|
||||
|---|---|---|
|
||||
| 라벨 | — | 팝업 추가(기존 유지) |
|
||||
| 빈 곳 | 있음 | 팝업 전체 닫기(재생 토글 안 함) |
|
||||
| 빈 곳 | 없음 | 재생/정지 토글 |
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
## 참고
|
||||
- 팝업의 '거리' 값은 클릭 시점 고정(위치만 추적). 필요 시 RAF 에서 거리도 실시간 갱신 가능.
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~470k / output ~9k tokens
|
||||
@@ -0,0 +1,28 @@
|
||||
# 폴더 선택 불가 회귀 수정 — 영상 준비 전 캔버스 입력 비활성
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**원인 도입**: [2026-06-24_1212_라벨팝업-다중-추적-클릭동작.md](2026-06-24_1212_라벨팝업-다중-추적-클릭동작.md)
|
||||
|
||||
## 증상
|
||||
폴더 선택이 안 되어 시작 불가.
|
||||
|
||||
## 원인 (회귀)
|
||||
직전 작업에서 오버레이 캔버스를 **항상 `pointer-events:auto`** 로 변경(영상이 클릭을 처리 안 해
|
||||
캔버스가 전담하도록). 그런데 영상이 없을 때 표시되는 **폴더 선택/드롭 안내 UI**(z-auto)를
|
||||
캔버스(z-20)가 덮어 클릭을 가로채서, 폴더 선택 버튼이 안 눌림.
|
||||
|
||||
## 조치
|
||||
캔버스 입력 토글 effect에 **영상 준비 게이트** 추가:
|
||||
```ts
|
||||
if (!videoReady) { canvas.style.pointerEvents = 'none'; return; } // 폴더 선택/드롭 통과
|
||||
```
|
||||
- 영상 없음/로딩 중(`videoReady=false`) → 캔버스 입력 off → 하위 폴더 선택 UI 정상 클릭.
|
||||
- 영상 준비 후(`videoReady=true`) → 캔버스 auto → 라벨 팝업/빈클릭 재생토글 등 정상.
|
||||
- effect 의존성에 `videoReady` 추가.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 4분
|
||||
**Context 사용량**: input ~490k / output ~3k tokens
|
||||
@@ -0,0 +1,29 @@
|
||||
# 팝업을 라벨 아래로 + 라벨 글자도 클릭 가능(바운딩박스 히트테스트)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1212_라벨팝업-다중-추적-클릭동작.md](2026-06-24_1212_라벨팝업-다중-추적-클릭동작.md)
|
||||
|
||||
## 요구
|
||||
1. 팝업을 POI 라벨 **아래**에 위치.
|
||||
2. POI **아이콘뿐 아니라 글자**도 클릭으로 선택 가능.
|
||||
|
||||
## 구현
|
||||
### 글자 클릭(바운딩박스 히트테스트)
|
||||
기존 히트테스트는 마커 점 기준 반경(0.04 정규)이라 글자(마커 오른쪽 텍스트)는 안 잡혔음.
|
||||
- RAF draw 에서 라벨을 그릴 때 **아이콘+글자 영역의 화면 px 박스**를 수집(`ctx.measureText`로 글자폭):
|
||||
· 측점: `x0=마커−8 … x1=글자끝+2`, `y=중심±12`.
|
||||
· POI: `x0=십자마커−r … x1=글자끝+2`, `y=중심±13`.
|
||||
- `labelHitRef`(프레임마다 갱신)에 박스 배열 저장. 라벨 없으면 빈 배열.
|
||||
- `hitTestLabel(nx,ny)`: 정규좌표를 cover px 로 변환 후 박스 포함 검사(나중에 그린 라벨 우선).
|
||||
→ 클릭/호버 모두 아이콘·글자 어디를 눌러도 선택. 호버 손모양 커서도 글자 위에서 동작.
|
||||
|
||||
### 팝업 라벨 아래 배치
|
||||
- 위치 공식 `left=sx−10, top=sy+16`(마커 중심 아래). 초기 렌더·RAF 추적 모두 동일.
|
||||
(이전 `sx+14, sy−10`(우측) → 라벨 아래로 변경.)
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~510k / output ~5k tokens
|
||||
@@ -0,0 +1,34 @@
|
||||
# 팝업이 라벨보다 늦게 사라지고, 닫힘 후 첫 클릭이 무시되던 문제
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1220_팝업-라벨아래-글자클릭.md](2026-06-24_1220_팝업-라벨아래-글자클릭.md)
|
||||
|
||||
## 증상
|
||||
1. 라벨이 화면에서 사라졌는데 팝업이 ~1초 더 보이다가 사라짐.
|
||||
2. 팝업이 (라벨 따라) 사라진 뒤, 화면 첫 클릭은 영상이 안 멈추고 두 번째 클릭에 멈춤.
|
||||
|
||||
## 원인 (동일 뿌리)
|
||||
`object-fit:cover` 로 영상이 컨테이너 밖까지 확장(크롭)되므로, 라벨이 **보이는 컨테이너에서
|
||||
잘려 사라져도 영상-정규좌표상으론 아직 존재**(pyRaw<1.02). 그래서:
|
||||
- 팝업이 화면 가장자리에 클램프된 채 라벨이 영상 끝(1.02)에 갈 때까지 남음(~1초).
|
||||
- 그 팝업이 `infoPopups` state 에 살아 있어 `hasPopupRef=true` → 빈 곳 첫 클릭이 그 '유령 팝업'을
|
||||
닫는 데 쓰이고(토글 안 함), 두 번째 클릭에야 재생 토글.
|
||||
|
||||
## 조치 — 라벨이 컨테이너를 벗어나면 팝업 state 에서 제거
|
||||
RAF 팝업 위치 갱신에서, 라벨이 없거나 마커 화면좌표(sx,sy)가 **컨테이너 밖**(±16px)이면 '사라짐':
|
||||
```ts
|
||||
const off = !disp || sx < -16 || sx > W+16 || sy < -16 || sy > H+16;
|
||||
if (off) { miss++; if (miss>=4) toRemove.push(id); else hide; return; }
|
||||
...
|
||||
if (toRemove.length) setInfoPopups(prev => prev.filter(p => !toRemove.includes(p.id)));
|
||||
```
|
||||
- 4프레임(~0.07s) 연속 밖이면 **state 에서 제거**(라벨과 거의 동시에 사라짐). 짧은 깜빡임 유예.
|
||||
- state 에서 빠지므로 `hasPopupRef` 즉시 false → 다음 빈 클릭이 바로 재생 토글(2번 문제 해결).
|
||||
- `popupMissRef`(id별 연속 밖 카운터)로 일시적 갭에 의한 조기 제거 방지.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~535k / output ~4k tokens
|
||||
@@ -0,0 +1,36 @@
|
||||
# POI/측점 라벨 이동 부드럽게 — 라인처럼 매 프레임 연속 포즈로 직접 투영
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 증상
|
||||
재생 중 드론 카메라가 움직일 때 POI/측점 라벨 이동이 부드럽지 않음(라인은 부드러움).
|
||||
|
||||
## 원인
|
||||
- **라인(중심선·궤적)**: RAF 가 매 프레임 `poseAt(estFrame)`(연속 보간 포즈)로 직접 투영 → 부드러움.
|
||||
- **라벨**: precompute 가 **정수 프레임별 화면좌표**를 저장하고, RAF 는 baseFrame↔baseFrame+1
|
||||
사이를 선형 보간 + EMA. 프레임키가 estFrame 과 안 맞으면(밀도/오프셋) 폴백(frac=0, 보간 없음)
|
||||
→ 동기 프레임 단위(거친 timeupdate)로 점프 → 덜 부드러움.
|
||||
|
||||
## 조치 — 라벨도 라인과 동일하게 연속 포즈로 직접 투영
|
||||
precompute 의 역할을 **'가시성/겹침 결정'만** 으로 한정하고, 좌표는 RAF 가 매 프레임 재투영.
|
||||
- `LabelCache` 저장 형식: 화면좌표(sx,sy)/(x,y) → **월드좌표**.
|
||||
· 측점: `{title, lat, lon, z}` (선로 스냅 좌표).
|
||||
· POI: `{title, category, lat, lon, gz}` (gz=지면고도; override/최근접 선로표고).
|
||||
- precompute: 기존대로 거리·겹침·클립 판정(화면좌표는 판정용 임시), 통과분만 월드좌표로 저장.
|
||||
- RAF: `dronePose = poseAt(estFrame)` 한 번 구해 라인·라벨 공통 사용.
|
||||
· 측점: `toCameraCoords(dronePose, lat, lon, z)` → `pixelFromCamera` → 화면좌표.
|
||||
· POI: `pz = 드론높이모드 ? dronePose.alt−지오이드−이격 : gz` → 동일 투영.
|
||||
· 기존 `smoothStep`(이상치 거부+EMA) 유지 → emaAlpha 슬라이더로 추가 평활 가능(기본 1.0=통과).
|
||||
- 정수프레임 보간/labelsB/title 짝짓기/프레임키 의존 제거.
|
||||
|
||||
## 효과
|
||||
- 라벨이 라인과 동일한 연속 포즈로 움직여 **부드럽게 이동**(드론 밀도/프레임키 무관).
|
||||
- 성능: RAF 는 가시집합(소수)만 투영(겹침/최근접은 precompute) → 가벼움.
|
||||
- 가시집합 멤버십만 baseFrame 기준(최대 ~1프레임 지연)·위치는 연속 → 체감 영향 없음.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 18분
|
||||
**Context 사용량**: input ~580k / output ~9k tokens
|
||||
@@ -0,0 +1,33 @@
|
||||
# 출입문(출입문번호)을 영상에 POI처럼 표출
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`, `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 배경
|
||||
QGIS(qgs)·gpkg 의 **출입문(출입문번호)** 레이어를 영상에 표시하고 싶다는 요청. 데이터는
|
||||
`building/05)출입문번호.csv` 에 존재(기존엔 기본 표시 제외). D:\ 데이터 직접 확인:
|
||||
컬럼 `본부,선별,역구간,latitude,longitude,시설팀,출입문번호,...,z,측점`.
|
||||
|
||||
## 구현
|
||||
- **`parseAccessDoors(files)`** 추가(`geoData.ts`): `building/05)출입문번호.csv`(UTF-8 BOM) →
|
||||
`GeoPoint{ title=출입문번호, category='출입문', lat/lon, z, type='poi' }`.
|
||||
헤더명 우선(폴백 인덱스 latitude=3/longitude=4/출입문번호=6/z=12), 위경도 범위검증, 번호 빈 행 스킵.
|
||||
- `loadFolderGeoData`: Promise.all 에 `parseAccessDoors` 추가, **pois 에 concat** → 영상 오버레이가
|
||||
POI 와 동일 파이프라인(거리필터·연속포즈 투영·클릭 팝업·지면고도)으로 표출.
|
||||
- `StationOverlay`: `CATEGORY_EMOJI['출입문']='🚪'`.
|
||||
|
||||
## 결과
|
||||
- 이 영상 구간(회덕-대전조차장) CSV엔 번호 보유 출입문 4건(5-122,6-007,6-009,6-010) → 🚪 라벨 표출.
|
||||
(번호 없는 행은 라벨 대상 아님 → 스킵.)
|
||||
- 표고는 기본(지면고도) 모드에서 최근접 측점표고 사용(CSV z=67.391 상수는 미사용).
|
||||
- POI 거리 범위(기본 300m)·겹침억제·클릭 속성팝업 모두 동일 적용.
|
||||
|
||||
## 데이터 접근 참고 (WSL /mnt/d)
|
||||
- gpkg `…_layers.gpkg`(출입문 526행 전체)·DEM `(B080)공개DEM/36710.img`(KGD2002/Unified, 90m,
|
||||
구간 포함)·xlsx 모두 판독 가능(sqlite3/rasterio/python3). 필요 시 로컬 DEM 으로 지면고도 대체 가능.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~620k / output ~7k tokens
|
||||
@@ -0,0 +1,38 @@
|
||||
# POI 클릭 팝업에 원본 CSV 속성 표시 (KMZ 정보표 형태)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/types/geo.ts`, `client/src/utils/geoData.ts`,
|
||||
`client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 요구
|
||||
KMZ 팝업처럼 POI(출입문 등)의 본부·선별·역구간·시설팀·측점 등 **원본 속성**을 라벨 클릭 팝업에 표시.
|
||||
|
||||
## 구현
|
||||
### 데이터: 원본 CSV 컬럼을 props 로 보존
|
||||
- `GeoPoint`/`RouteStructure` 에 `props?: { k: string; v: string }[]` 추가(순서 보존).
|
||||
- 헬퍼 `cleanHeader()`(트림+BOM)·`rowProps(header,row)`(비어있지 않은 헤더:값 배열) 추가.
|
||||
- 적용 파서(전부 비어있지 않은 모든 컬럼 저장):
|
||||
· `parseAccessDoors`(출입문) · `parsePois`(_POI.csv + 02)지장물) · `parseStructures`(03)교량/04)터널/06)구교).
|
||||
- `StationOverlay` 구조물→GeoPoint 매핑에 `props: s.props` 전달.
|
||||
|
||||
### 팝업 렌더 (StationOverlay)
|
||||
- `InfoPopup` 에 `props?` 추가, 클릭 시 `obj.props` 캡처.
|
||||
- 팝업 본문을 `grid grid-cols-[auto_1fr]`(키/값) 로:
|
||||
· props 있으면 **모든 속성**(예 본부·선별·역구간·latitude·longitude·시설팀·출입문번호·5186x/y·X/Y/Z좌표·z·측점) + 드론거리.
|
||||
· props 없으면 기존 기본(종류/위도/경도/표고/거리).
|
||||
- `max-h-[45vh] overflow-y-auto`(속성 많을 때 스크롤).
|
||||
|
||||
## 효과
|
||||
- 출입문 6-007 클릭 → KMZ와 동일 항목(본부 대전충남 / 선별 경부선 / 역구간 회덕-대전조 / 시설팀
|
||||
대전시설팀 / 출입문번호 6-007 / 측점 (158K600) / z 67.391 …)이 팝업에 표시.
|
||||
- 지장물·_POI·교량/터널/구교도 각 CSV 속성 표시.
|
||||
|
||||
## 참고 (미구현)
|
||||
- KMZ의 "지면 고도 46.12m"(구글 DEM 실측)는 CSV에 없음 → 로컬 공개 DEM(`36710.img`, rasterio)으로
|
||||
좌표별 지면고도를 채워 팝업/표고에 쓰는 건 별도 작업으로 가능.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~660k / output ~7k tokens
|
||||
@@ -0,0 +1,20 @@
|
||||
# 팝업 — KML(원본 CSV) 속성만 표시 (계산값 제거)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1453_POI팝업-원본CSV속성표시.md](2026-06-24_1453_POI팝업-원본CSV속성표시.md)
|
||||
|
||||
## 요구
|
||||
팝업에는 KML 파일에 있는 정보만 표출. 그 외(계산값)는 표출 불필요.
|
||||
|
||||
## 조치
|
||||
팝업 본문에서 앱 계산/부가 항목 제거, **원본 속성(props)만** 표시:
|
||||
- 제거: `거리(드론 기준)`, `종류·카테고리`, `표고(보정)` 등 계산/가공 행.
|
||||
- props 있는 항목(출입문·지장물·_POI·교량/터널/구교): **props(원본 CSV=KML 항목)만** 렌더.
|
||||
- props 없는 항목(측점 등): 좌표(위도/경도)만 최소 표시.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 4분
|
||||
**Context 사용량**: input ~690k / output ~2k tokens
|
||||
@@ -0,0 +1,38 @@
|
||||
# KMZ 직접 로딩 — POI/구조물을 원본 KMZ에서 추출 (CSV 추출 중복 제거)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`, `client/package.json`(+fflate)
|
||||
|
||||
## 배경/요구
|
||||
지금까지 KMZ → CSV 추출(중복 작업) → 앱이 CSV 로드. 사용자: "KMZ가 원본이니 앱이 KMZ를 직접
|
||||
읽으면 끝날 일." → 앱이 KMZ를 직접 파싱하도록 변경.
|
||||
|
||||
## KMZ 구조 (확인)
|
||||
`<base>.kmz` = `doc.kml`(zip, deflate). 폴더가 building CSV와 1:1:
|
||||
`02)지장물·03)교량·04)터널·05)출입문번호·06)구교`. 각 Placemark = 좌표 + description HTML표(속성).
|
||||
측점(01)은 KMZ에 **없음** → 측점은 계속 CSV.
|
||||
- 02)지장물 69 · 03)교량 13(신대천교(복) 등) · 04)터널 2 · 05)출입문 8 · 06)구교 9 = 101 Placemark.
|
||||
|
||||
## 구현
|
||||
- `fflate` 추가(브라우저 zip 해제, 8KB).
|
||||
- **`parseKmz(files)`**: 루트 .kmz → `unzipSync` → doc.kml → `DOMParser`.
|
||||
폴더 트리 재귀(`walk`)로 Placemark 의 **최내곽 폴더명**을 분류 키로 사용:
|
||||
· 교량/터널/구교 → `RouteStructure`(연장(m)=lengthM, 시설종별=grade).
|
||||
· 출입문 → `GeoPoint{category:'출입문', title=출입문번호}`.
|
||||
· 그 외(02)지장물) → `GeoPoint{category=category_clean}` (철도역/역사는 StationOverlay가 영상 제외).
|
||||
좌표는 `<coordinates>` 우선, 속성은 description CDATA(HTML표)를 `<b>키</b>…<td>값</td>` 정규식 →
|
||||
`props`(팝업 표시·KML 항목 그대로).
|
||||
- `loadFolderGeoData`: `parseKmz` 추가. **KMZ 있고 내용 있으면** 그걸로 pois/structures 사용
|
||||
(지장물·출입문·교량/터널/구교), 없으면 기존 CSV 폴백. **측점·역사·route.json·드론프레임은 CSV 유지.**
|
||||
|
||||
## 결과
|
||||
- KMZ 로드 시: POI 77(지장물 69 + 출입문 8) · 구조물 24(교량 13 + 터널 2 + 구교 9).
|
||||
- 팝업 속성 = KMZ description표 그대로. 신대천교(복) 등 교량도 영상 표출.
|
||||
- 더 이상 02~06 CSV 추출 불필요(측점 CSV/소스, 지장물_역사 CSV만 유지).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
- Python으로 동일 폴더→분류 로직 검증(101 Placemark, 폴더별 수 일치).
|
||||
|
||||
**소요 시간**: 약 22분
|
||||
**Context 사용량**: input ~740k / output ~12k tokens
|
||||
@@ -0,0 +1,36 @@
|
||||
# KMZ source=KAKAO_RAIL(철도역) → 스테이션바 표출
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`
|
||||
**관련**: [2026-06-24_1603_KMZ직접로딩-CSV중복제거.md](2026-06-24_1603_KMZ직접로딩-CSV중복제거.md)
|
||||
|
||||
## 요구
|
||||
KMZ에서 POI를 가져올 때 `source` 타입이 **KAKAO_RAIL**(철도역)인 항목은 영상이 아니라
|
||||
**하단 스테이션바**에 표시.
|
||||
|
||||
## 조치 (`parseKmz`)
|
||||
02)지장물 폴더 Placemark 처리에서 source/category 로 분기:
|
||||
```ts
|
||||
const src = pget('source');
|
||||
const cat = pget('category_clean') || '지장물';
|
||||
if (src === 'KAKAO_RAIL' || cat === '철도역' || cat === '역사') {
|
||||
// 역사 구조물로 → 스테이션바(Timeline 원형 마커). 영상 미표시.
|
||||
structures.push({ id:`역사-…`, type:'station', category:'역사', name, lat, lon, props });
|
||||
} else {
|
||||
pois.push({ ... category: cat ... }); // 일반 POI(영상)
|
||||
}
|
||||
```
|
||||
- `RouteStructure{type:'station', category:'역사'}` 는 StationBar 가 이미 처리(`cats=['교량','터널','역사']`).
|
||||
- 위치 배치: 역사는 `station`값 없이 **lat/lon → 드론 최근접 통과 px**(`pxPassesTo`)로 배치(기존 역사와 동일).
|
||||
- pois 에서 빠지므로 영상엔 안 뜨고(StationOverlay 가 역사 미표출), 스테이션바에만 표출.
|
||||
|
||||
## 참고
|
||||
- 현재 샘플 KMZ(`하행_회덕-대전조차장.kmz`)는 source 가 VWORLD_ROAD/KAKAO_POI 뿐이라 KAKAO_RAIL 없음
|
||||
→ 이 데이터엔 변화 없음. 철도역이 포함된 데이터(이전/타 구간 KMZ에 회덕화물역=KAKAO_RAIL 존재)에서 적용됨.
|
||||
- category_clean='철도역'/'역사' 도 함께 역사로 라우팅(태깅 방식 무관하게 철도역 포착).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~770k / output ~5k tokens
|
||||
@@ -0,0 +1,43 @@
|
||||
# 구글 새 KML 형식 파싱 + 스테이션바 구조물 클릭 팝업
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`, `client/src/stationbar/StationBar.tsx`,
|
||||
`client/src/stationbar/components/Timeline/Timeline.tsx`
|
||||
**관련**: [2026-06-24_1630_KMZ-KAKAO_RAIL-스테이션바라우팅.md](2026-06-24_1630_KMZ-KAKAO_RAIL-스테이션바라우팅.md)
|
||||
|
||||
## 증상
|
||||
- 회덕화물역 속성이 위도/경도만 보임(=props 비어 no-props 폴백). 영상에 남아있음(스테이션바로 안 감).
|
||||
- 구글에서 다시 받은 KML의 형식이 달라짐.
|
||||
|
||||
## 원인 (새 KML 형식)
|
||||
구글어스에서 재저장된 Placemark description 표가 변형됨:
|
||||
- 옛: `<b>source</b></td><td>VWORLD_ROAD</td>`
|
||||
- 새: `<table><tbody>…<b>source</b><br></td><td>KAKAO_RAIL<br></td>` (`<tbody>`·`</b>` 뒤 `<br>`·`>` 엔티티)
|
||||
기존 정규식 `<b>키</b>\s*</td>…` 이 `</b>` 뒤 `<br>` 때문에 매칭 실패 → props 빈값 →
|
||||
source 못 읽어 KAKAO_RAIL 라우팅도 실패(영상에 남음).
|
||||
또 사용자는 .kmz 가 아니라 **bare `doc.kml`** 을 폴더에 받음.
|
||||
|
||||
## 조치
|
||||
### 1) description 파싱을 행/셀 단위로 견고화 (`geoData.ts`)
|
||||
`<tr>` 안 두 `<td>` 셀에서 태그 제거+엔티티 디코드(`stripHtml`) → (키,값). 옛/새 형식 모두 처리.
|
||||
→ 회덕화물역 17개 속성 전부 추출(source=KAKAO_RAIL 포함) 검증. KAKAO_RAIL→스테이션바 라우팅 정상화.
|
||||
|
||||
### 2) bare .kml 지원 (`parseKmz`)
|
||||
폴더 루트의 **`.kml` 우선**(구글 직접 다운로드, UTF-8 `await file.text()`), 없으면 `.kmz`(zip) 해제.
|
||||
|
||||
### 3) 스테이션바 구조물 클릭 팝업 (`StationBar.tsx`·`Timeline.tsx`)
|
||||
역사(회덕화물역)는 스테이션바 원형 아이콘만 있어 속성이 안 보였음 →
|
||||
- `StructMark` 에 `props` 추가(구조물 마크 빌드 시 `s.props` 전달).
|
||||
- Timeline: 구조물 마커 위 클릭 핫스팟(zIndex 21, seekArea 위) → 클릭 시 **속성 팝업**.
|
||||
팝업은 클릭 좌표 기준 `position:fixed`(트랙 래퍼 transform 영향 회피), 배경 클릭 시 닫힘.
|
||||
- 역사/교량/터널 모두 적용(구교는 영상에서 처리).
|
||||
|
||||
## 결과
|
||||
- 회덕화물역: KAKAO_RAIL → 스테이션바(원형) + 클릭 시 전체 KML 속성 팝업.
|
||||
- 새 형식 KML(또는 .kmz) 모두 속성 정상 추출.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) 통과. Python으로 새 형식 행/셀 파싱 → 17속성 추출 확인.
|
||||
|
||||
**소요 시간**: 약 25분
|
||||
**Context 사용량**: input ~820k / output ~12k tokens
|
||||
@@ -0,0 +1,39 @@
|
||||
# 철도역(KAKAO_RAIL) — 영상 POI + 스테이션바 양쪽 표출 (모든 POI 영상 표시)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`, `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1647_새KML형식파싱+스테이션바구조물팝업.md](2026-06-24_1647_새KML형식파싱+스테이션바구조물팝업.md)
|
||||
|
||||
## 요구 (정정)
|
||||
- 회덕화물역 POI 가 영상에 안 보임 → **모든 POI 는 영상에 보여야 함**(철도역 포함).
|
||||
- KAKAO_RAIL 은 영상 POI 에 **더해** 스테이션바에도 표시(둘 다).
|
||||
|
||||
## 직전 오류
|
||||
KAKAO_RAIL 을 스테이션바로 **라우팅(영상 제외)** 했음 → 회덕화물역이 영상에서 빠짐. 요구는 양쪽 표출.
|
||||
|
||||
## 조치
|
||||
### parseKmz — 철도역을 pois 에 항상 + 스테이션바에 추가
|
||||
```ts
|
||||
pois.push({ title:name, category:cat, lat, lon, z, type:'poi', props }); // 모든 POI 영상
|
||||
if (src === 'KAKAO_RAIL' || cat === '철도역' || cat === '역사')
|
||||
structures.push({ type:'station', category:'역사', name, lat, lon, props }); // + 스테이션바
|
||||
```
|
||||
### StationOverlay — 철도역/역사 영상 제외 필터 제거
|
||||
```diff
|
||||
- const allPoi = allPoisRef.current
|
||||
- .filter(p => p.category !== '철도역' && p.category !== '역사')
|
||||
- .concat(allStructuresRef.current);
|
||||
+ const allPoi = allPoisRef.current.concat(allStructuresRef.current); // 모든 POI 표출
|
||||
```
|
||||
- `CATEGORY_EMOJI['철도역']='🚉'` 추가.
|
||||
|
||||
## 결과
|
||||
- 회덕화물역: **영상 POI(🚉 + 전체 KML 속성 팝업)** + **스테이션바(원형 마커, 클릭 시 속성 팝업)** 동시.
|
||||
- 역사 구조물은 영상 구조물 필터(구교/교량/터널)에 미포함이라 영상엔 POI 1개만(중복 없음).
|
||||
- 다른 모든 POI(지장물 등)도 영상 표출(제외 없음).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~880k / output ~5k tokens
|
||||
@@ -0,0 +1,23 @@
|
||||
# POI 라벨 = KML 구분 → title → name 순
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`
|
||||
|
||||
## 요구
|
||||
POI 라벨을 KML의 `구분` 또는 `title` 로 표시, 그런 항목이 없으면 지금처럼 `name`(placemark name).
|
||||
|
||||
## 조치 (`parseKmz`)
|
||||
지오코딩 POI(02)지장물·철도역) 라벨 우선순위:
|
||||
```ts
|
||||
const label = pget('구분') || pget('title') || name;
|
||||
pois.push({ title: label, ... });
|
||||
```
|
||||
- 철도역의 스테이션바(역사) name 도 동일 label 사용(일관).
|
||||
- 출입문(=출입문번호)·구조물(교량/터널=구분, 구교=시설물명)은 각자 라벨 유지(이미 적절).
|
||||
- CSV 폴백(`parsePois`)은 이미 'title'/'명칭' 컬럼 사용.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 4분
|
||||
**Context 사용량**: input ~920k / output ~2k tokens
|
||||
@@ -0,0 +1,33 @@
|
||||
# 1배속에서 POI 라벨·팝업 떨림 제거 — 속도 적응형 평활(One Euro 방식)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## 증상
|
||||
빠른 배속에선 POI 라벨/팝업이 부드러운데, **1배속(느린 이동)에선 흔들려 보임**.
|
||||
|
||||
## 원인
|
||||
라벨 화면좌표에 **일정 진폭의 떨림(GPS/투영 잔여 노이즈)**이 있음. 빠른 배속은 프레임당 이동량이
|
||||
커서 떨림이 묻히고, 1배속은 이동이 작아 떨림이 도드라짐. 화면 EMA(`emaAlpha`)가 1.0(평활 거의 없음)
|
||||
이라 그대로 노출됨.
|
||||
|
||||
## 조치 — `smoothStep` 을 속도 적응형으로(One Euro 필터 방식)
|
||||
고정 alpha → **속도에 따라 가변**:
|
||||
- 속도 벡터를 평활(`vx,vy` EMA, β=0.25). 떨림은 매 프레임 방향이 번갈아 → 평활속도≈0,
|
||||
실제 이동은 방향 일관 → 평활속도 큼. (떨림과 실제 이동을 속도로 구분)
|
||||
- `alpha = min(maxAlpha, MIN(0.12) + (maxAlpha−0.12)·min(1, speed/SPEED_REF(0.01)))`
|
||||
- 정지/떨림(speed≈0) → alpha≈0.12 → **강한 평활(1배속 안정)**.
|
||||
- 빠른 이동(speed≥0.01) → alpha→maxAlpha(패널 EMA α=1.0) → **즉시 추종(빠른 배속 지연 없음)**.
|
||||
- 이상치 거부(REJECT_DIST)·연속한계는 유지. `DispPos` 에 vx/vy 추가.
|
||||
|
||||
## 효과
|
||||
- 1배속·먼 POI(떨림 잘 보임) → 강하게 평활돼 안정.
|
||||
- 가까운/빠른 이동 POI → 즉시 추종(지연 없음).
|
||||
- 팝업도 라벨 displayed 좌표를 따라가므로 함께 안정.
|
||||
- 패널 **EMA α** 슬라이더 = 빠를 때의 추종 상한(기본 1.0).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~960k / output ~4k tokens
|
||||
@@ -0,0 +1,24 @@
|
||||
# 속도적응 평활 튜닝값을 패널 슬라이더로 노출 (떨림억제·반응속도)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/components/overlay/StationOverlay.tsx`
|
||||
**관련**: [2026-06-24_1758_라벨팝업-속도적응평활-1배속떨림제거.md](2026-06-24_1758_라벨팝업-속도적응평활-1배속떨림제거.md)
|
||||
|
||||
## 요구
|
||||
직전에 추가한 `SMOOTH_MIN_ALPHA(0.12)`/`SMOOTH_SPEED_REF(0.01)` 을 어디서 조절? (코드 상수였음)
|
||||
→ 패널에서 실시간 조절 가능하게.
|
||||
|
||||
## 조치
|
||||
- `smoothStep` 시그니처에 `minAlpha, speedRef` 파라미터 추가(모듈 상수 제거). `SMOOTH_VEL_BETA`만 상수 유지.
|
||||
- 상태/ref `smoothMinAlpha`(0.12)·`smoothSpeedRef`(0.010) + `DISPLAY_DEFAULTS` 추가.
|
||||
- RAF 의 smoothStep 2개 호출에 `smoothMinAlphaRef.current, smoothSpeedRefRef.current` 전달.
|
||||
- 화면표시 옵션 패널 "스무딩" 섹션에 슬라이더 2개:
|
||||
· **떨림억제**(min α, 0.02~0.6) — 낮을수록 1배속 떨림 강하게 평활.
|
||||
· **반응속도**(speedRef, 0.002~0.04) — 클수록 느린 이동까지 부드럽게, 줄이면 빨리 반응.
|
||||
- "기본값" 버튼 리셋 + `ghivideo:calib` 영속화(저장/복원)에 포함. EMA α 툴팁을 "빠를 때 추종 상한"으로 정리.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~1010k / output ~5k tokens
|
||||
@@ -0,0 +1,34 @@
|
||||
# 스테이션바 구조물명 — 긴 명칭 2줄 + 첫/마지막 항목 양끝 정렬
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/stationbar/components/Timeline/Timeline.tsx`,
|
||||
`client/src/stationbar/components/Timeline/Timeline.module.scss`
|
||||
|
||||
## 요구
|
||||
1. 스테이션바 구조물명이 길면 두 줄로 표시.
|
||||
2. 첫 항목과 마지막 항목은 바 끝에 위치(끝에서 잘리지 않게).
|
||||
|
||||
## 조치
|
||||
### 1) 긴 명칭 2줄 (`.structName`)
|
||||
구조물명 라벨에 `structName` 클래스 추가:
|
||||
```scss
|
||||
.structName { width:96px; white-space:normal; text-align:center; font-size:12px;
|
||||
line-height:12.5px; word-break:break-all; overflow-wrap:anywhere;
|
||||
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
||||
```
|
||||
- 폭 96px 고정 → 길면 자동 줄바꿈, 공백 없는 한글도 끊어 2줄. 최대 2줄(넘으면 …).
|
||||
- 폰트 16.5→12px 로 줄여 2줄이 라벨행(26px)에 들어가게.
|
||||
|
||||
### 2) 첫/마지막 항목 양끝 정렬
|
||||
`structs`(px 정렬)의 i===0 은 `anchorStart`(좌측 정렬, 좌끝이 px), 마지막은 `anchorEnd`(우측
|
||||
정렬, 우끝이 px) → 가운데정렬 시 바 가장자리에서 잘리던 것 방지(기존 anchor 클래스 재사용).
|
||||
|
||||
## 참고
|
||||
- 요구2는 "첫/마지막 라벨을 바 끝에서 안 잘리게 정렬"로 해석. 만약 '첫/마지막 시설을 바의
|
||||
물리적 좌우 끝 좌표로 이동'을 의도했다면 다른 구현 필요(확인 후 조정 가능).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~1060k / output ~5k tokens
|
||||
@@ -0,0 +1,27 @@
|
||||
# 스테이션바 첫/마지막 항목을 바 시작·끝 지점으로 이동(px 스냅)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/stationbar/components/Timeline/Timeline.tsx`
|
||||
**관련**: [2026-06-24_1921_스테이션바-구조물명2줄+양끝정렬.md](2026-06-24_1921_스테이션바-구조물명2줄+양끝정렬.md)
|
||||
|
||||
## 요구 (요청2 확정)
|
||||
첫 번째 항목은 스테이션바 **시작점**, 마지막 항목은 **끝점**으로 각각 이동해 표시.
|
||||
|
||||
## 조치
|
||||
dedup(px 정렬) 후 첫/마지막 항목의 px 를 바 양끝 좌표로 스냅:
|
||||
```ts
|
||||
const BAR_START = TRACK_START_PX; // 297
|
||||
const BAR_END = TRACK_START_PX + TRACK_WIDTH_PX; // 1806.5
|
||||
const structsRaw = dedup(structures, 90);
|
||||
const structs = structsRaw.map((s, i) =>
|
||||
i === 0 ? { ...s, px: BAR_START } : i === structsRaw.length - 1 ? { ...s, px: BAR_END } : s);
|
||||
```
|
||||
- s.px 좌표계는 트랙 래퍼 공간(297~1806.5, seekArea/pxAtTime 와 동일).
|
||||
- 아이콘·측점값·구조물명·클릭핫스팟이 모두 s.px 사용 → 항목 전체가 양끝으로 함께 이동.
|
||||
- 직전 작업의 첫=좌측정렬/마지막=우측정렬(anchorStart/End)과 결합 → 끝에서 안 잘림.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 5분
|
||||
**Context 사용량**: input ~1110k / output ~3k tokens
|
||||
@@ -0,0 +1,23 @@
|
||||
# 스테이션바 구조물명 — 아이콘 가운데 정렬 + 폰트 원래 크기 복원
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/stationbar/components/Timeline/Timeline.tsx`,
|
||||
`client/src/stationbar/components/Timeline/Timeline.module.scss`
|
||||
**관련**: [2026-06-24_1933_스테이션바-첫마지막항목-바양끝스냅.md](2026-06-24_1933_스테이션바-첫마지막항목-바양끝스냅.md)
|
||||
|
||||
## 요구
|
||||
- 글자도 해당 아이콘 가운데로 이동(가운데 정렬).
|
||||
- 글자 크기를 이전(2줄 작업)처럼 작게 하지 말 것.
|
||||
|
||||
## 조치
|
||||
- Timeline: 첫/마지막 좌·우 정렬(anchorStart/End) 제거 → 모든 구조물명 라벨 **가운데 정렬**
|
||||
(segmentLabel 기본 translateX(-50%)). 아이콘 px 스냅(첫=바시작/마지막=바끝)은 유지 →
|
||||
라벨이 아이콘 가운데에 함께 위치.
|
||||
- `.structName`: `font-size:12px`/`line-height:12.5px` 작은 폰트 제거 → **라벨행 기본 16.5px 유지**.
|
||||
2줄 래핑(width 92px·line-clamp 2·word-break)·line-height 18px 는 유지.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 4분
|
||||
**Context 사용량**: input ~1150k / output ~3k tokens
|
||||
@@ -0,0 +1,30 @@
|
||||
# 측점/역사 파일을 root 에서도 인식 (building 폴더 삭제 가능)
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/utils/geoData.ts`
|
||||
|
||||
## 배경
|
||||
사용자가 v3.0 폴더에서 `01)측점.csv` 를 root(영상 옆)로 옮기고 building 폴더(02~06)를 지우려 함.
|
||||
그런데 기존 코드는 측점/역사를 `building/` 안에서만 찾아 → root 이동 시 측점 미인식(중심선 깨짐).
|
||||
|
||||
## 조치
|
||||
- `parseStations`: 측점 파일을 building → **root 어디든** 찾도록(`includes('01)측점')`/`'측점'`).
|
||||
- `parseHistoricStations`: 역사 파일도 `isInBuilding` 제한 제거(root 허용).
|
||||
- 드론 CSV 선택(`parseDroneFrames`)은 `<base>.csv` 우선이라 root 에 측점 CSV 가 같이 있어도 영향 없음.
|
||||
|
||||
## 파일 필요 여부 정리 (웹앱 기준)
|
||||
| 파일 | 필요 |
|
||||
|---|---|
|
||||
| `<영상>.MP4` | ✅ 필수 |
|
||||
| **`<영상>.csv`** (frame_cnt,lat,lon,alt,yaw,pitch,roll,focal) = 드론 프레임 | ✅ **필수** |
|
||||
| `01)측점.csv` (root 또는 building) | ✅ 필수(KMZ에 없음) |
|
||||
| `<영상>.kmz` | ✅ POI/구조물 |
|
||||
| building/02~06)*.csv | ⭕ 삭제 가능(KMZ 대체, 폴백용) |
|
||||
| `<영상>_POI.csv` | ⭕ 폴백(삭제 가능) |
|
||||
| `.srt` / `.qgs` / `.qgz` / `_구조물추출_Z.xlsx` | ⭕ 웹앱 미사용 |
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~1210k / output ~5k tokens
|
||||
@@ -0,0 +1,24 @@
|
||||
# 스테이션바 라벨 폭 확대(짧은 명칭 1줄) + POI 팝업 1배속 물결 제거
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/stationbar/components/Timeline/Timeline.tsx`,
|
||||
`client/src/stationbar/components/Timeline/Timeline.module.scss`,
|
||||
`client/src/components/overlay/StationOverlay.tsx`
|
||||
|
||||
## A. 짧은 명칭은 한 줄로
|
||||
증상: "법동가도교 (하)" 처럼 한 줄에 들어갈 명칭이 2줄로 줄바꿈됨(폭 92px 가 좁아서).
|
||||
- `.structName` width 92→**140px** (≈8자 1줄). 긴 명칭만 2줄로 래핑.
|
||||
- 겹침 방지: `dedup(structures, 90→130)` (라벨 폭에 맞춤).
|
||||
|
||||
## B. POI 팝업 1배속 물결(떨림) 제거
|
||||
증상: 1배속 재생 시 팝업(큰 텍스트 박스)이 물결침.
|
||||
원인: 라벨의 잔떨림 + 매 프레임 **소수픽셀** 위치 갱신 → 텍스트 안티앨리어싱 셰이딩이 물결로 보임.
|
||||
- 팝업 위치에 **별도 EMA(α=0.18, 무겁게)** 적용(`popupPosRef`) + **정수 픽셀 반올림**(`Math.round`).
|
||||
→ 라벨을 따라가되 잔떨림은 흡수, 소수픽셀 셰이딩 제거. (팝업 제거 시 ref 정리)
|
||||
- 팝업은 정보 박스라 약간의 지연은 무방.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~1290k / output ~6k tokens
|
||||
@@ -0,0 +1,35 @@
|
||||
# 스테이션바 시/종점 역명 = route.json name 의 '-' 좌/우 + 좌상단 타이틀 routeInfo 매핑 확인
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/stationbar/StationBar.tsx`
|
||||
|
||||
## 요구
|
||||
1. 스테이션바 시작은 측점값(157K900 등)이 아니라 route.json `name`("회덕-대전조차장")의 '-' **왼쪽=회덕**,
|
||||
끝은 **오른쪽=대전조차장**.
|
||||
2. 좌측 상단 타이틀은 각 항목에 맞는 routeInfo 변수로.
|
||||
|
||||
## 조치
|
||||
### 1) 시/종점 역명 = name '-' 분리 (StationBar)
|
||||
```ts
|
||||
const nm = routeMeta?.routeInfo?.name ?? ''; // "회덕-대전조차장"
|
||||
const dash = nm.indexOf('-');
|
||||
const leftName = dash >= 0 ? nm.slice(0, dash).trim() : ''; // 회덕
|
||||
const rightName = dash >= 0 ? nm.slice(dash + 1).trim() : ''; // 대전조차장
|
||||
startStationName: leftName || routeInfo.startStationName || 측점첫;
|
||||
endStationName: rightName || routeInfo.endStationName || 측점끝;
|
||||
```
|
||||
- name 분리값 우선(요구). 없으면 startStationName/endStationName → 측점 첫/끝 폴백.
|
||||
|
||||
### 2) 좌상단 타이틀 (변경 없음 — 이미 매핑)
|
||||
`RouteInfoOverlay` 가 이미 routeInfo 각 변수를 매핑: 방향=direction, 노선명=name, 연장=lengthKm,
|
||||
소요=durationSec. route.json 만 폴더에 있으면 표출됨 → 추가 작업 불필요.
|
||||
|
||||
## 확인
|
||||
- 폴더의 `하행)회덕-대전조차장.route.json` 은 UTF-8 정상. `parseRouteMeta`(file.text, base.route.json 매칭)로 로드.
|
||||
- name="회덕-대전조차장", direction="하 행", lengthKm=4.25, durationSec=672(=11분12초).
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~1350k / output ~5k tokens
|
||||
@@ -0,0 +1,28 @@
|
||||
# 시/종점 역명 = route.json start/endStationName + 구조물 스냅 제거 + 종점 역명 안 잘림
|
||||
|
||||
**날짜**: 2026-06-24
|
||||
**수정 파일**: `client/src/stationbar/StationBar.tsx`,
|
||||
`client/src/stationbar/components/Timeline/Timeline.tsx`,
|
||||
`client/src/stationbar/components/Timeline/Timeline.module.scss`
|
||||
**관련**: [2026-06-24_2032_스테이션바시종점-routejson-name분리.md](2026-06-24_2032_스테이션바시종점-routejson-name분리.md)
|
||||
|
||||
## 요구 (갱신)
|
||||
- 시작 = route.json `startStationName`, 끝 = `endStationName`.
|
||||
- 구조물 아이콘/라벨은 이전처럼(실제 위치).
|
||||
|
||||
## 조치
|
||||
1. **시/종점 역명**: name 분리 → `startStationName`/`endStationName` 직접 사용으로 되돌림(없으면 측점 첫/끝 폴백).
|
||||
→ route.json 에 원하는 값(예 startStationName:"회덕", endStationName:"대전조차장")을 넣으면 그대로 표시.
|
||||
2. **구조물 px 스냅 제거**: 첫/마지막을 바 양끝으로 옮기던 로직 삭제 → 구조물은 실제 통과 px 그대로.
|
||||
(시/종점 역명이 바 끝을 담당하므로 구조물 스냅 불필요. 시작점 겹침도 해소.)
|
||||
3. **종점 역명 안 잘림 유지**: `.stationEnd` 를 `left: track-end+40` → `right: 8px` 로 변경
|
||||
(긴 종점명 "대전조차장" 등도 우측 끝 기준 안쪽으로 펼쳐 화면 밖 안 잘림).
|
||||
|
||||
## 유지 (이전 사용자 요청)
|
||||
- 구조물명 긴 건 2줄/짧은 건 1줄(width 140), 아이콘 가운데 정렬, 폰트 16.5px, 겹침간격 130.
|
||||
|
||||
## 검증
|
||||
- `npm run build -w client` (node20) tsc + vite build 통과.
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~1410k / output ~5k tokens
|
||||
@@ -0,0 +1,38 @@
|
||||
# 스테이션바 종점역(대전조차장) 누락 수정 + 라벨 우선순위 정리
|
||||
|
||||
**소요 시간**: 25분
|
||||
**Context 사용량**: input 약 130k / output 약 9k tokens
|
||||
|
||||
## 증상
|
||||
하단 스테이션바의 마지막 항목에 종점역 **대전조차장**(한국철도공사대전조차장역)이 표시되지 않음.
|
||||
|
||||
## 원인
|
||||
[Timeline.tsx](../../client/src/stationbar/components/Timeline/Timeline.tsx)의 구조물 라벨 겹침 방지 로직:
|
||||
|
||||
```ts
|
||||
const structs = dedup(structures, 130);
|
||||
```
|
||||
|
||||
기존 `dedup`은 px 오름차순으로 정렬해 **130px 클러스터마다 가장 왼쪽 1개만** 유지했다.
|
||||
|
||||
실데이터(하행)회덕-대전조차장 v2.0) 기준:
|
||||
- 종점역 `대전조차장역` 좌표: lat 36.371101 / lon 127.421776
|
||||
- 그 직전 `법동가도교`(상·하·인상 3건): lat ~36.3730 (≈200m 앞, 트랙 1509.5px 중 ~70px)
|
||||
|
||||
→ 법동가도교가 먼저(왼쪽) 유지되고, 130px 이내인 종점역이 dedup으로 탈락.
|
||||
|
||||
## 사용자 확정 규칙 (라벨 겹침 우선순위)
|
||||
- **화면상(영상) 라벨**: 겹침 처리 동작 + 겹칠 경우 **KAKAO_RAIL(철도역/역사) 우선 표출**.
|
||||
- **하단 스테이션바**: 겹쳐도 무방 → 역사는 dedup 대상에서 제외, 항상 표출.
|
||||
|
||||
## 조치
|
||||
1. 하단 스테이션바 [Timeline.tsx](../../client/src/stationbar/components/Timeline/Timeline.tsx)
|
||||
- 역사(역, KAKAO_RAIL)는 dedup에서 제외하고 **항상 표출**.
|
||||
- 교량/터널만 기존 130px 겹침 방지 유지(라벨 텍스트가 길어 겹침 방지 필요).
|
||||
2. 영상 오버레이 [StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx)
|
||||
- POI 겹침 억제 정렬을 `dist`만 → **railRank(철도역/역사=0) → dist** 로 변경.
|
||||
- 겹치면 KAKAO_RAIL 라벨이 유지되고 일반 POI가 숨겨짐.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit -p client/tsconfig.json` → 통과(exit 0)
|
||||
- 실데이터 좌표·거리 계산으로 클러스터링 원인 확인
|
||||
@@ -0,0 +1,27 @@
|
||||
# 스테이션바 첫·마지막 역 양끝 고정 + 누락 원인(빌드) 해결 + pm2 배포
|
||||
|
||||
**소요 시간**: 70분
|
||||
**Context 사용량**: input 약 320k / output 약 22k tokens
|
||||
|
||||
## 1. 대전조차장 누락의 진짜 원인 = 빌드 미반영
|
||||
- 데이터(v3.0 KMZ)·파싱(parseKmz)·코드 수정 모두 정상이었음. 실데이터 시뮬레이션으로 마커가 px 1777 생성·유지됨을 검증.
|
||||
- 화면에 안 뜬 이유: 서버가 **6/24 빌드된 `client/dist`** 를 서빙 → 6/25 수정 미반영.
|
||||
- 조치: `npm run build -w client` 재빌드 후 `pm2 start ecosystem.config.js` (PORT 55000). 정상 표출 확인.
|
||||
|
||||
## 2. 렌더 필터 수정 (앞 단계, 누적 반영)
|
||||
- [Timeline.tsx](../../client/src/stationbar/components/Timeline/Timeline.tsx): 역사(역, KAKAO_RAIL)는 dedup 제외 → 항상 표출.
|
||||
- [StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx): POI 겹침 억제 정렬을 railRank(철도역/역사 우선)→dist 로 변경.
|
||||
|
||||
## 3. 첫·마지막 스테이션 양끝 고정 (이번 요청)
|
||||
- [StationBar.tsx](../../client/src/stationbar/StationBar.tsx) `structureMarks` 에 `snapStationEnds` 헬퍼 추가.
|
||||
- 역사 마커 중 **최좌측 → TRACK_START_PX(297)**, **최우측 → TRACK_END_PX(1806.5)** 로 스냅.
|
||||
- 역사 2개 미만이면 미적용. 동일 역 복수 통과 시 극단 마커만 이동.
|
||||
- 두 return 경로(storeStructures / POI 폴백) 모두 적용.
|
||||
- 예: 회덕화물역(353.9→297), 한국철도공사대전조차장역(1777→1806.5).
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, 클라이언트 재빌드 성공, pm2 `ghiVideo` online, HTTP 200.
|
||||
|
||||
## 운영 메모
|
||||
- 실행: nvm Node 20 활성화 → `npm run build -w client` → `pm2 restart ghiVideo`.
|
||||
- 서버는 `client/dist` 정적 서빙 → **소스 수정 후 반드시 클라이언트 재빌드**해야 화면 반영됨.
|
||||
@@ -0,0 +1,27 @@
|
||||
# 스테이션바 구조물명 라벨 2줄 균등 분할
|
||||
|
||||
**소요 시간**: 15분
|
||||
**Context 사용량**: input 약 350k / output 약 25k tokens
|
||||
|
||||
## 요청
|
||||
스테이션바 항목 이름이 2줄로 표출될 때 위/아래 글자 수가 최대한 같게 배치(옆 라벨과 간격 최대 확보).
|
||||
|
||||
## 조치
|
||||
1. [Timeline.tsx](../../client/src/stationbar/components/Timeline/Timeline.tsx) `splitStructLabel` 추가
|
||||
- 추정 폭 ≤ 8.2자(≈140px/16.5px, CJK 1.0·기타 0.55)면 1줄 유지.
|
||||
- 길면 글자수 절반 지점에서 2줄 분할(균등) → 최대 줄 폭 최소화.
|
||||
- 분할점이 괄호 `(...)` 내부를 가르면 괄호 시작 앞으로 이동(괄호 통째 아랫줄).
|
||||
- 라벨 렌더: `lines.length===1 ? text : <>{l0}<br/>{l1}</>`.
|
||||
2. [Timeline.module.scss](../../client/src/stationbar/components/Timeline/Timeline.module.scss) `.structName`
|
||||
- `width:140px` 자동 줄바꿈 → `width:max-content; max-width:140px; white-space:nowrap`.
|
||||
- JS가 `<br>`로 줄을 제어하므로 word-break/line-clamp 제거. 박스가 내용 폭에 맞게 줄어 간격 확보.
|
||||
|
||||
## 검증
|
||||
- 한국철도공사대전조차장역 → 한국철도공사 / 대전조차장역 (6/6)
|
||||
- 법동가도교(상,인상,고속) → 법동가도교 / (상,인상,고속)
|
||||
- 회덕제1가도교(인상) → 회덕제1가도 / 교(인상) (6/5)
|
||||
- 회덕화물역·신대천교(복)·회덕터널(상) 등 → 1줄
|
||||
- `tsc` 통과, 클라이언트 재빌드, pm2 `ghiVideo` 재기동, HTTP 200.
|
||||
|
||||
## 비고
|
||||
- 한 줄 임계값(8.2자)·괄호 처리 방식은 조정 가능.
|
||||
@@ -0,0 +1,26 @@
|
||||
# POI 라벨(구분) · 시설등급 기타 · 회덕화물역 km 검증 · 컴팩트 팝업
|
||||
|
||||
**소요 시간**: 40분
|
||||
**Context 사용량**: input 약 470k / output 약 38k tokens
|
||||
|
||||
## 1. 화면 POI 라벨 — '구분' 우선
|
||||
- [StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) `guboonOf()` 추가.
|
||||
- poiCand/poiMarkers/LabelCache 에 `label` 필드(=구분 값 있으면 그 값, 없으면 title) 추가.
|
||||
- 캔버스 라벨이 `poiA.label` 사용. (matching/override 키는 title 유지.)
|
||||
|
||||
## 2. 시설등급에 '기타' 추가
|
||||
- [settingsStore.ts](../../client/src/store/settingsStore.ts) `KNOWN_GRADES`에 '기타' 추가, `DEFAULT_GRADE_FILTER['기타']=true`.
|
||||
- VideoPlayer 필터 UI는 `KNOWN_GRADES.map`이라 자동 노출. isGradeVisible 규칙상 '기타'도 토글 대상이 됨.
|
||||
|
||||
## 3. 회덕화물역 스테이션바 값 = 157K990 (검증)
|
||||
- 실데이터 검증: 회덕화물역 최근접 프레임 762(16.6m)·재접근 3647(44.7m) **둘 다 chain→157k990**. 마커 km 라벨은 이미 157k990 산출.
|
||||
- 주의(미해결): 회덕화물역이 양끝 스냅으로 t=0(좌측 끝)에 붙어 있어, **마커로 seek 시 커서는 영상 시작(≈157k900) 위치**를 보임 → 마커값(157k990)과 불일치. 시작점도 방식1(시간축 재기준화)로 맞추려면 추가 작업 필요(사용자 확인 대기).
|
||||
|
||||
## 4. 화면 POI 컴팩트 팝업 + 클릭 시 전체
|
||||
- `compactFieldsOf()` 추가: 시설종별 / 구조형식(상부구조형식) / 연장(m) 중 존재하는 것만.
|
||||
- poiMarkers에 `compact` 미리계산. 캔버스에서 라벨 아래 반투명 박스로 **항상 표시**.
|
||||
- 히트박스를 컴팩트 박스까지 확장 → 클릭 시 기존 전체 속성 DOM 팝업 오픈.
|
||||
- 성능: DOM 팝업 남발 대신 캔버스 드로잉(라벨과 함께 매 프레임, 구조물만 대상).
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, 클라이언트 재빌드, pm2 ghiVideo 재기동, HTTP 200.
|
||||
@@ -0,0 +1,36 @@
|
||||
# 동일 항목 묶음 브래킷 표시
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~55k / output ~6k tokens
|
||||
|
||||
## 결론
|
||||
스테이션바 타임라인에서 같은 항목(동일 역명)을 드론이 여러 번 통과할 때, 두 번째 참고 이미지처럼
|
||||
**점선 브래킷(각 통과 수직 드롭 + 하단 수평 연결선)** 으로 묶고 **클러스터 중앙에 라벨 1개** 만
|
||||
표시하도록 변경.
|
||||
|
||||
## 근거 / 변경 내용
|
||||
- 파일: `client/src/stationbar/components/Timeline/Timeline.tsx`
|
||||
- 기존: 다중통과 시 `labelEntries`(마지막 통과 라벨 1개 + 나머지 중앙 라벨 1개) + 통과별 점선 틱(`leaderPxs`)
|
||||
→ 같은 항목인데 라벨이 2개로 쪼개져 표시됨.
|
||||
- 변경: `labelGroups` 로 재구성.
|
||||
- 같은 title 통과들을 px 간격(`GROUP_GAP_PX = 350`)으로 **클러스터 분할** — 가까운 통과는 한 묶음,
|
||||
멀리 떨어진 통과(예: 대전조차장 두 차례 진입)는 별도 묶음 → 각자 브래킷/라벨 1개.
|
||||
- 클러스터당: 각 통과 px에 수직 드롭(top 46, h4) + 양끝을 잇는 수평 점선(top 50) + 중앙(centerPx) 라벨 1개.
|
||||
- 단일 통과는 브래킷 없이 자기 위치 라벨 1개(기존 동작 유지).
|
||||
- 색(passed/neutral) 판정 기준을 통과 px → 클러스터 `centerPx` 로 변경. 미도착(unreached)은 묶음 내
|
||||
하나라도 있으면 neutral 유지.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과 (node 20 환경).
|
||||
|
||||
## 후속 수정 (가로 점선 미표시 → 거리 클러스터링 제거)
|
||||
- 1차 구현은 `GROUP_GAP_PX = 350` 으로 동명 통과를 거리별 클러스터로 분할 → 대전조차장역 두 통과가
|
||||
~1050px 떨어져 각각 단일 묶음이 되어 브래킷(가로 점선)이 안 나옴.
|
||||
- 수정: 거리 클러스터링 제거. **동명(title) 통과는 거리와 무관하게 하나로 묶어** lo~hi 전체에
|
||||
수평 점선 + 각 통과 드롭 + 중앙 라벨 1개를 그림. (사용자 요청: "가로 점선이 보여야해")
|
||||
|
||||
## 다음 단계 참고
|
||||
- 브래킷 수직 위치(드롭 46~50, 수평 50, 라벨행 51)는 트랙 하단(46.5)/labelRow(51) 기준. 라벨 폰트/행간
|
||||
변경 시 함께 점검.
|
||||
- 동명 통과가 매우 멀리 떨어진 경우 중앙 라벨이 빈 트랙 중앙에 놓일 수 있음(가로 점선으로 연결됨). 추후
|
||||
묶음 분할이 다시 필요하면 거리 임계값 방식을 재도입할 것.
|
||||
@@ -0,0 +1,35 @@
|
||||
# 스테이션바 거리축(이동량 기반) 전환
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~90k / output ~9k tokens
|
||||
|
||||
## 결론
|
||||
스테이션바 커서/구간/마커의 가로축을 **시간(프레임) 선형 → 누적 이동거리(거리축)** 로 전환.
|
||||
드론이 호버링(이동 없음)하면 커서가 멈추고, 실제로 이동할 때만 커서가 진행한다. (사용자 요청 A안)
|
||||
|
||||
## 근거 / 변경 내용
|
||||
- 파일: `client/src/stationbar/StationBar.tsx`
|
||||
- 기존: `px = 시작 + (currentTime/duration) × 트랙폭` — 순수 시간 정비례. 호버 중에도 커서 전진(배지 측점값은 정지 → 불일치).
|
||||
- 변경: 시간↔진행도 매핑을 거리축으로 교체. 핵심은 매핑 함수 한 곳에 집중되어 커서·구간색·구조물 마커가 일관되게 따라옴.
|
||||
1. `progRef`: 프레임별 `{time, frac}` 배열. `frac` = 누적 이동거리 비율(0~1, 단조 비감소).
|
||||
2. 진행도 산출(viewedRef 빌드 useEffect): 연속 측점값(chain)을 ±W(8프레임) 이동평균으로 평활(GPS 지터 제거) 후
|
||||
프레임간 `|Δ|` 누적 = 실제 이동량. 전체로 정규화.
|
||||
3. `cumFracAtTime(t)`: 시간→진행비율(이진탐색+선형보간). progRef 미준비 시 시간선형 폴백.
|
||||
4. `timeAtFrac(f)`: 역변환(진행비율→시간) — 바 클릭/측점 seek 용.
|
||||
5. `pxAtTime(t) = 시작 + cumFracAtTime(t) × 트랙폭` — 모든 배치가 공유.
|
||||
6. 라이브 커서 RAF, `seekToTrackX` 도 거리축 함수 사용.
|
||||
|
||||
## 동작
|
||||
- 호버(이동 無): frac 정체 → 커서 정지. 이동: frac 증가 → 좌→우 진행.
|
||||
- 전진/후진 모두 이동량으로 더해져 커서는 단조 진행(방향은 기존 주황/청록 구간색으로 구분).
|
||||
- 배지 측점값과 커서가 동일 데이터(연속 측점값)에서 파생 → 표시·이동 일관.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과 + `client/dist` 빌드 완료. (pm2 ghiVideo 는 정적 서빙 → 재시작 불필요, 브라우저 새로고침)
|
||||
|
||||
## 다음 단계 참고 / 되돌리기
|
||||
- 사용자가 "이상하면 원복" 예정. 되돌리려면 `pxAtTime`/`cumFracAtTime`/`timeAtFrac` 를 기존 시간선형
|
||||
(`(t/duration)`)으로 복귀시키고 `progRef`/평활 블록 제거하면 됨.
|
||||
- 평활 반폭 `W=8`, 호버 판정은 평활로 처리(별도 데드밴드 없음). 호버 중 커서가 미세하게 기어가면 W를 키우거나
|
||||
소량 데드밴드 추가.
|
||||
- 부작용: 호버가 길어도 바에서 폭 0으로 수렴 → 바 클릭으로 호버 내부 시간 미세 seek 불가(영상 타임라인 사용).
|
||||
@@ -0,0 +1,43 @@
|
||||
# 스테이션바 측점거리(체이니지) 비례 축 전환 (B안)
|
||||
|
||||
**소요 시간**: 약 30분
|
||||
**Context 사용량**: input ~150k / output ~14k tokens
|
||||
|
||||
## 결론
|
||||
스테이션바 x축을 직전 A안(누적 이동거리) → **측점값(체이니지) 비례 축**으로 전환.
|
||||
바 가로위치가 실제 측점거리에 비례하고, 커서는 드론의 현재 측점 위치를 표시한다(멈추면 정지, 후진하면 좌로 이동).
|
||||
측점 10m 차이 구조물이 바에서도 가깝게 표시된다. 색: '진행색 + 커서색' 안 선택.
|
||||
|
||||
## 배경
|
||||
- A안(누적 이동거리)은 두 구조물이 측점상 10m여도 그 사이 드론이 앞뒤로 많이 이동하면 누적거리가 커져 바에서 멀게 표시됨 → 사용자 지적.
|
||||
- 요구: 바는 측점거리에 비례, 재생 중 실제 드론 위치를 바에 표시.
|
||||
|
||||
## 변경 내용 (client/src/stationbar/StationBar.tsx)
|
||||
- `chainRef = {time, chain(평활), minC, maxC}` 생성(ready useEffect). chain 은 ±8프레임 이동평균(지터 제거).
|
||||
- 매핑: `pxAtChain(측점) = START + clamp((c-minC)/(maxC-minC)) * timeTrackWidth`,
|
||||
`chainAtTime(t)`(이진탐색+보간), `pxAtTime(t) = pxAtChain(chainAtTime(t))`. 커서·구조물 공유.
|
||||
- 커서: `progressPx = pxAtTime(currentTime)`, RAF 도 `pxAtTime(liveTime)`.
|
||||
- 색(측점축은 방향 리본 표현 불가): `trackGradient`=주황 음영 단일, `trackGradientIdle`=회색 단일,
|
||||
`dividers=[]`. 방향은 `currentReverse`(측점값 0.4s 전 대비 감소=후진)로 커서 색만 구분.
|
||||
- 구조물 중복 제거: `dedupeMarksByTitle`(title 당 1개) — 측점축에선 동명 다중통과가 한 자리에 겹침.
|
||||
→ Timeline 다중통과 묶음 점선(브래킷)은 자연 소멸.
|
||||
- seek: 클릭 px → 목표 측점 → 허용오차 내 프레임 중 현재 시간에 가장 가까운 통과로 onSeek.
|
||||
- `barSegments` 는 렌더 미사용(쿼리 useMemo 는 kmLabels 위해 유지), `cumFracAtTime/timeAtFrac/progRef` 제거.
|
||||
|
||||
## 후속 수정 — 축 범위 '노선 고정' (데이터 무관 절대 측점)
|
||||
- 문제: 축 범위를 드론 커버 구간 [minC,maxC] 로 잡아 영상마다 양끝 측점이 바뀜 → 같은 구조물이 다른
|
||||
위치/측점으로 표시됨(사용자 지적: "어떤 데이터로 재생하느냐에 따라 측점이 바뀌면 안돼").
|
||||
- 수정:
|
||||
1. 축 범위 minC/maxC = stationLine(역 POI, km 오름차순) 첫/끝 측점값 → 노선 고정. 드론 chain 은
|
||||
projectChainage 로 이 범위 안에 들어옴.
|
||||
2. 구조물 px = pxAtChain(구조물 측점값) → 통과 시각이 아닌 '절대 측점'으로 배치(마일리지 라벨과 일치,
|
||||
영상 달라도 동일 위치). dedupeMarksByTitle 로 동명 1개.
|
||||
- 효과: 좌(낮은 측점)→우(높은 측점) 고정 자. 구조물 위치 불변, 커서만 드론 현재 측점을 가리킴.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과 + client 빌드 완료.
|
||||
|
||||
## 알려진 한계 / 다음 단계
|
||||
- 진짜 비례라 측점이 매우 가까운 라벨끼리 겹쳐 보일 수 있음(법동가도교/대전조차장역). 필요시 라벨 충돌회피 후처리.
|
||||
- 방향 전진/후진 색 리본 제거됨(커서 색으로만 방향). endGapPx(미도착 종점역) 우측 밴드/표식은 유지.
|
||||
- 이상 시 A안(누적거리 축) 또는 그 이전 시간선형으로 되돌릴 수 있음. 핵심 매핑은 pxAtChain/chainAtTime/pxAtTime 3개에 집중.
|
||||
@@ -0,0 +1,40 @@
|
||||
# 역사(역) 통과 마커 누락 수정 — 좌표근접 합집합 보강
|
||||
|
||||
**소요 시간**: 약 30분
|
||||
**Context 사용량**: input ~150k / output ~9k tokens
|
||||
|
||||
## 결론
|
||||
대전조차장역처럼 같은 역을 여러 번 지날 때, 일부 통과(예: 162k080 재진입)에 스테이션바 동그라미(역사 원형 마커)가
|
||||
빠지던 문제를 수정. 측점-기준 탐지에 더해 **좌표근접(`pxPassesTo`) 통과를 합집합으로 보강**해 누락된 재진입까지 마커를 찍는다.
|
||||
|
||||
## 원인
|
||||
- 파일: `client/src/stationbar/StationBar.tsx` (`structureMarks`)
|
||||
- 역사(대전조차장역)는 `geoData`에서 좌표(lat/lon)만 있고 측점값(`station`)이 없는 RouteStructure 로 들어온다.
|
||||
- 통과 탐지가 `projectChainage(좌표)` 한 값으로 `pxPassesAtMileage` 만 수행.
|
||||
- 탐지 체인이 `if (!passes.length)` **단락 평가**라, 측점-기준이 통과를 하나라도 찾으면 좌표근접 탐지가 안 돌아감.
|
||||
- 종점/조차장 구역은 측점선(폴리라인) 투영이 불안정 → 한 통과는 투영 측점이 ±`stationTolerance`(기본 20m)
|
||||
안에 들어와 잡히지만, 다른 통과는 20m 밖으로 벗어나 누락. 첫 통과를 이미 찾았으니 좌표근접 보강도 안 돌아 영영 빠짐.
|
||||
|
||||
## 변경 내용
|
||||
- `cat === '역사'` 이고 lat/lon 이 있으면, 측점-기준 탐지 결과에 `pxPassesTo(lat, lon, off)` 통과를 합집합으로 추가.
|
||||
- 같은 통과(거의 동일 px)는 24px 임계로 중복 제거 → 멀리 떨어진 실제 재진입만 추가됨.
|
||||
(TRACK_WIDTH_PX = 1509.5, 24px ≈ 1.6% → 동일 best 프레임 중복만 합쳐지고 떨어진 재진입은 보존)
|
||||
- km 라벨 동작(stationKmVal 우선)은 기존 유지 — 이번 수정은 '마커 누락'만 대상.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` + `vite build` 통과 (node v20.20.2, `~/.nvm/versions/node/v20.20.2/bin`).
|
||||
- 사용자 화면(포트 55000)에서 폴더 재선택 후 162k080 재진입 동그라미 표출 **확인 완료**.
|
||||
|
||||
## ★ 진짜 블로커 — 빌드/서빙 구조 (이번 세션 최대 교훈)
|
||||
- 증상: 소스를 여러 번 고쳐도 화면이 전혀 안 바뀜("여전히 빠져있어" 반복).
|
||||
- 원인: 사용자가 보는 화면은 dev 서버가 아니라 **PM2 앱 `ghiVideo`(포트 55000)** 이고,
|
||||
`server/dist/.../app.js` 가 `express.static(client/dist)` 로 **빌드 결과를 서빙**한다(ecosystem.config.js).
|
||||
→ `client/src` 만 고치면 절대 반영 안 됨. `client/dist` 가 6/26 빌드로 고정돼 있었음.
|
||||
- 형제 디렉토리 혼선도 있었음: `videoplayer`(PM2, 5173), `defVideo`(PM2) 가 함께 떠 있음. 작업 경로는 GhiVideo.
|
||||
- 해결: `npm run build -w client` (node 20) 로 `client/dist` 갱신 → 강력 새로고침(Ctrl+Shift+R) → 폴더 재선택.
|
||||
- 메모리에 구조 기록: `ghivideo-serve-build`.
|
||||
|
||||
## 참고 / 후속
|
||||
- `stationTolerance` 타입 주석(geo.ts:139)은 "기본 40"으로 적혀 있으나 실제 코드 기본값은 `?? 20`. 불일치 정리 권장.
|
||||
- 좌표근접 탐지 임계는 `pxPassesTo` 내부 `Math.max(100, gmin*3)`(StationBar.tsx:499) — 측점 tolerance(20m)와 별개.
|
||||
- 출발점 부근 '조기 통과' 오탐 우려는 역사 마커에 한해 합집합으로 한정 → 일반 측점-기준 동작에는 영향 없음.
|
||||
@@ -0,0 +1,23 @@
|
||||
# 측점값 라벨 겹침 숨김 (먼저 나온 것만 표시)
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~190k / output ~11k tokens
|
||||
|
||||
## 결론
|
||||
스테이션바에서 측점값(예: 157k990) 라벨이 근접 통과로 겹쳐 보일 때, px 오름차순으로 **먼저 나온(좌측) 라벨만
|
||||
표시하고 뒤엣것은 숨김** 처리. 아이콘(동그라미)과 점선 브래킷은 그대로 둔다(측점값 텍스트만 숨김).
|
||||
|
||||
## 변경 내용
|
||||
- 파일: `client/src/stationbar/components/Timeline/Timeline.tsx`
|
||||
- `structs`(px 오름차순) 직후 `showMileage: boolean[]` 산출:
|
||||
- `labelW(v) = v.length * 7.5 + 4` (13px bold + 2px stroke 대략 폭)
|
||||
- 직전 '표시한' 라벨과 중앙 간격이 `(lastW + w)/2` 미만이면 겹침으로 보고 숨김(`showMileage[i]=false`).
|
||||
- 표시한 경우에만 `lastPx/lastW` 갱신 → 연쇄 겹침도 누적 기준으로 정확히 판정.
|
||||
- `mileageRow` 렌더 조건을 `s.km >= 0` → `s.km >= 0 && showMileage[i]` 로 변경.
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드 → 55000 강력 새로고침으로 확인.
|
||||
|
||||
## 참고
|
||||
- 라벨은 trackWrapper transform 하위라 px 공간 비교가 시각 겹침과 동치(균일 스케일).
|
||||
- 폭 추정 상수(7.5/+4)는 폰트/스트로크 변경 시 재조정.
|
||||
@@ -0,0 +1,26 @@
|
||||
# 선형·드론궤적 토글을 재생바 버튼으로 이동
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~210k / output ~13k tokens
|
||||
|
||||
## 결론
|
||||
영상 오버레이의 "선형(중심선)"·"드론 궤적" 표시 토글을 **하단 재생바(VideoPlayer)의 겹침제외 버튼 왼쪽**에
|
||||
버튼 2개로 추가하고, 화면표시 옵션 창의 기존 동일 기능 버튼 2개는 제거. 버튼 폭은 겹침제외와 동일(min-w-[96px]).
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/store/settingsStore.ts`
|
||||
- `showCenterline`(기본 true), `showDronePath`(기본 true) + 각 setter 추가. persist 로 지속.
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
- 로컬 `useState` 였던 `showCenterline`/`showDronePath` → 설정 스토어 구독으로 전환(refs/리셋은 그대로).
|
||||
- 화면표시 옵션 창의 선형/드론궤적 토글 버튼 2개 제거. (드론궤적 경로표고·투명도 슬라이더는 유지)
|
||||
- `client/src/components/player/VideoPlayer.tsx`
|
||||
- 스토어 셀렉터 추가, 겹침제외 버튼 바로 앞(좌측)에 `선형 ON/OFF`·`드론궤적 ON/OFF` 버튼 삽입.
|
||||
- 스타일: 겹침제외와 동일 폭(min-w-[96px]), 색은 기능별(선형=red, 드론궤적=sky).
|
||||
- 노출 조건은 겹침제외와 동일: 영상제어 ON + geoLoaded.
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드 → 55000 강력 새로고침 확인.
|
||||
|
||||
## 참고
|
||||
- 상태를 스토어로 올려 바 버튼과 오버레이 렌더가 단일 소스 공유.
|
||||
- '기본값' 리셋(StationOverlay)도 스토어 setter 로 동작 유지.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 경로표고·투명도 항상 표시 + 겹침제외 버튼 숨김
|
||||
|
||||
**소요 시간**: 약 5분
|
||||
**Context 사용량**: input ~225k / output ~14k tokens
|
||||
|
||||
## 결론
|
||||
화면표시 옵션 창의 "경로 표고"·"투명도" 슬라이더를 드론궤적 ON/OFF와 무관하게 항상 표시하도록 변경.
|
||||
재생바의 "겹침제외 ON/OFF" 버튼은 UI에서 숨김(기능/상태는 보존).
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
- 경로표고·투명도 ParamRow 를 감싸던 `{showDronePath && (...)}` 게이트 제거 → 항상 렌더.
|
||||
- `client/src/components/player/VideoPlayer.tsx`
|
||||
- 겹침제외 토글 버튼 JSX 주석 처리(숨김). `poiOverlapExclude` 상태는 스토어에 그대로 존재(기본 ON),
|
||||
StationOverlay/StationBar/RoutePanel 이 계속 구독. 버튼만 비표시. 다시 보이려면 주석 해제.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` + `vite build` 통과(node 20). client/dist 재빌드(11:11).
|
||||
|
||||
## 참고
|
||||
- 겹침제외 selector(poiOverlapExclude/setPoiOverlapExclude)는 VideoPlayer 에 선언만 남아 미사용 상태지만
|
||||
tsc 통과(noUnusedLocals 미적용). 버튼 재노출 대비 보존.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 좌측 패널 버튼 바 이동 + 측점진단 버튼 삭제(GPS·고도 항상 표시)
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~245k / output ~16k tokens
|
||||
|
||||
## 결론
|
||||
- "좌측 노선 패널 표시" 버튼을 화면표시 옵션 창에서 제거하고, 하단 재생바의 **선형 버튼 왼편**에
|
||||
"좌측 패널 ON/OFF" 버튼으로 이동(동일 폭 min-w-[96px], emerald).
|
||||
- "측점 진단" 토글 버튼 삭제. 드론 GPS·절대고도 HUD 는 **항상 표시**(토글 없음)로 변경.
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
- 표시 토글 영역에서 "좌측 노선 패널", "측점 진단" 버튼 2개 제거.
|
||||
- 미사용된 `showRoutePanel`/`setShowRoutePanel`, `showStationDiag`/`setShowStationDiag` 선언 제거.
|
||||
- `client/src/components/player/VideoPlayer.tsx`
|
||||
- `setShowRoutePanel` 셀렉터 추가, 선형 버튼 앞(좌측)에 "좌측 패널 ON/OFF" 버튼 삽입.
|
||||
- `stationDiag` useMemo 의 `!showStationDiag` 게이트 제거 → 프레임만 있으면 항상 계산.
|
||||
- GPS·고도 HUD 렌더의 `showStationDiag &&` 게이트 제거 → 항상 표시.
|
||||
- 미사용된 `showStationDiag` 셀렉터 제거.
|
||||
|
||||
## 바 버튼 순서 (영상제어 ON + geoLoaded)
|
||||
좌측 패널 → 선형 → 드론궤적 (겹침제외는 이전 요청대로 숨김)
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드(11:15).
|
||||
|
||||
## 참고
|
||||
- settingsStore 의 showStationDiag 필드는 남겨둠(다른 잠재 구독 대비). 현재 토글 UI 는 없음.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 재생바 요소 재배치 — 드론궤적+위치정보 인접, 시설등급 우측
|
||||
|
||||
**소요 시간**: 약 3분
|
||||
**Context 사용량**: input ~260k / output ~17k tokens
|
||||
|
||||
## 결론
|
||||
하단 재생바에서 드론 위치정보(GPS·고도) HUD 를 드론궤적 버튼 바로 우측에 붙이고, 시설등급 옵션 패널을
|
||||
그 오른편(맨 뒤)으로 옮겨 순서를 정리.
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/components/player/VideoPlayer.tsx`
|
||||
- 드론 위치정보 HUD(`{stationDiag && ...}`)를 시설등급 패널 뒤 → **드론궤적 버튼 직후**로 이동.
|
||||
- 시설등급 패널이 HUD 뒤(우측)에 오도록 순서 변경.
|
||||
|
||||
## 바 버튼/요소 순서 (영상제어 ON + geoLoaded)
|
||||
좌측 패널 → 선형 → 드론궤적 → 드론 위치정보(GPS·고도) → 시설등급
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드(11:19).
|
||||
@@ -0,0 +1,25 @@
|
||||
# 활성화 상태 색상 amber(겨자/오렌지)로 통일
|
||||
|
||||
**소요 시간**: 약 4분
|
||||
**Context 사용량**: input ~280k / output ~18k tokens
|
||||
|
||||
## 결론
|
||||
재생바·우측 토글의 활성화(ON/선택) 표시 색을 제각각(파랑/초록/빨강/하늘/회색)에서 **amber-400 + 검정 글씨**로
|
||||
통일. 사용자 요청(노란/겨자/오렌지 계열).
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/components/player/VideoPlayer.tsx`
|
||||
- 영상제어 ON: blue → `bg-amber-400 border-amber-300 text-black`
|
||||
- 배속 선택: `bg-blue-600 text-white` → `bg-amber-400 text-black`
|
||||
- 좌측 패널 / 선형 / 드론궤적 ON: emerald/red/sky → amber-400
|
||||
- 시설등급 체크박스: `accent-blue-500` → `accent-amber-400`
|
||||
- `client/src/components/overlay/StationOverlay.tsx`
|
||||
- "화면표시 옵션"(showDisplay) 활성: sky-700 → amber-400
|
||||
- "카메라 파라미터"(showControls) 활성: gray-700 → amber-400
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드(11:23).
|
||||
|
||||
## 참고
|
||||
- 비활성(OFF)은 기존 `bg-black/70 border-gray-600 text-gray-300` 유지.
|
||||
- 더 진하게 원하면 amber-500 으로 교체.
|
||||
@@ -0,0 +1,27 @@
|
||||
# 작업 보고서 md/html/pdf 3종 생성 + 변환 스크립트
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~310k / output ~21k tokens
|
||||
|
||||
## 결론
|
||||
오늘 작업 내용을 팀장 보고용 문서로 작성하고, **md(베이스) → html + pdf** 3종 산출물을 생성.
|
||||
재생성용 변환 스크립트와 스타일을 저장소에 추가.
|
||||
|
||||
## 산출물
|
||||
- `docs/보고서_2026-06-29_스테이션바_UI개선.md` (베이스)
|
||||
- `docs/보고서_2026-06-29_스테이션바_UI개선.html` (자체완결, CSS 인라인)
|
||||
- `docs/보고서_2026-06-29_스테이션바_UI개선.pdf` (A4 3p, 한글 임베드, 페이지번호)
|
||||
|
||||
## 추가 파일
|
||||
- `scripts/md2docs.sh` — `md2docs.sh <파일.md>` 로 같은 경로에 html+pdf 생성.
|
||||
- `docs/assets/report.css` — 보고서 스타일(앰버 헤더/표/@page).
|
||||
|
||||
## 변환 환경 (메모: report-doc-toolchain)
|
||||
- pandoc 2.11 `--self-contained` 로 md→html (CSS 인라인 임베드).
|
||||
- weasyprint 61.2 로 html→pdf. **pydyf 는 0.10 으로 핀**(0.11 은 API 변경으로 TypeError).
|
||||
- 한글 폰트: 환경에 CJK 폰트 부재 → Windows 맑은고딕(`/mnt/c/Windows/Fonts/malgun*.ttf`)을
|
||||
`~/.local/share/fonts` 로 복사 후 `fc-cache`. WSL 이라 가능.
|
||||
|
||||
## 검증
|
||||
- PDF 헤더 %PDF-1.7, 3페이지, `/FontFile`(Malgun 글리프 임베드) 확인 → 모든 뷰어에서 한글 정상.
|
||||
- scripts/md2docs.sh 재실행으로 동일 산출물 재현 확인.
|
||||
@@ -0,0 +1,27 @@
|
||||
# 측점 검색 순환 점프 + 입력값 유지/블러삭제
|
||||
|
||||
**소요 시간**: 약 12분
|
||||
**Context 사용량**: input ~360k / output ~24k tokens
|
||||
|
||||
## 결론
|
||||
측점값이 여러 곳(여러 통과)에 존재할 때, 측점 검색을 **통과방향 순환(find-next)** 으로 개선.
|
||||
Enter 반복으로 다음 통과로 이동, 끝에서 처음으로 순환. 입력값은 Enter 후 유지하고 blur 시 삭제.
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/stationbar/StationBar.tsx` — `handleJumpToMileage` 재작성
|
||||
- 입력 측점의 '모든 통과'를 허용오차(stationTolerance, 기본 20m) 내 연속구간=1통과로 묶어 시간 오름차순 수집.
|
||||
- 기준 시각(baseT)에서 통과방향(시간 증가) 다음 통과로 이동, 없으면 passes[0] 으로 순환.
|
||||
- `jumpRef`(km,time)로 연속 검색 판정: 같은 측점이고 커서가 직전 점프 위치 그대로(|Δ|<0.1s)일 때만
|
||||
연속(다음 통과). 재생/클릭으로 커서 이동했거나 다른 측점이면 현재 커서(라이브)에서 새로 시작.
|
||||
- 라이브 시각은 `timeRef?.current ?? currentTime`. 커버 범위 밖(±20m) 입력은 무시(기존 유지).
|
||||
- 허용오차 내 통과가 없으면 전역 최근접 1개로 폴백.
|
||||
- `client/src/stationbar/components/PlaybackControls/PlaybackControls.tsx`
|
||||
- Enter 시 `setQuery('')` 제거 → 입력값 유지(같은 값 재Enter 로 순환).
|
||||
- input `onBlur` → 입력값 삭제(다른 곳 클릭 시 언셀렉트).
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드(12:12).
|
||||
|
||||
## 참고
|
||||
- 순환 순서는 '시간(이동거리축 진행) 오름차순' = 화면상 좌→우.
|
||||
- 향후 입력창 옆 'n/m' 위치 표시·매칭 하이라이트를 추가하면 인지성 더 향상.
|
||||
@@ -0,0 +1,27 @@
|
||||
# 측점 검색을 '화면 마커' 기준으로 전환 (누락 통과까지 검색)
|
||||
|
||||
**소요 시간**: 약 7분
|
||||
**Context 사용량**: input ~390k / output ~26k tokens
|
||||
|
||||
## 결론
|
||||
측점 검색 점프가 일부 통과로 가지 못하던 문제 해결. 검색 후보를 chain 기준 → **화면에 보이는
|
||||
마커(structureMarks) 기준**으로 바꿔, 바에 표시된 모든 동그라미(좌표근접 탐지 통과 포함)를 순환하도록 함.
|
||||
|
||||
## 원인
|
||||
- 마커(structureMarks)는 측점기준 + 좌표근접(pxPassesTo) 합집합으로 통과를 찍음(조차장 등 chain 어긋난 통과 포함).
|
||||
- 그러나 검색(handleJumpToMileage)은 `|arr[i].chain - km| < TOL` 측점기준으로만 통과를 찾음
|
||||
→ chain 이 162080 에서 벗어난 통과(마커는 있음)는 검색이 못 감.
|
||||
|
||||
## 변경 내용 (`client/src/stationbar/StationBar.tsx`)
|
||||
- handleJumpToMileage 후보 산출 1순위:
|
||||
`structureMarks.filter(표시 10m 반올림값 == 입력값).map(px → timeAtFrac 로 시각 역변환)`.
|
||||
- 2순위(마커 없는 임의 측점): 기존 chain 연속구간 탐지 → 없으면 전역 최근접 폴백.
|
||||
- 시간 정렬 + 0.3s 근접 중복 제거 후, 통과방향 순환(기존 jumpRef 로직 유지).
|
||||
- deps 에 structureMarks, timeAtFrac, timeTrackWidth 추가.
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드(12:19).
|
||||
|
||||
## 참고
|
||||
- 매칭은 표시 라벨과 동일하게 10m 반올림 일치 → 사용자가 보는 값과 검색 일치.
|
||||
- 종점 미도착으로 px=TRACK_END 로 이동된 마커는 역변환 시 끝 시각이 됨(허용).
|
||||
@@ -0,0 +1,30 @@
|
||||
# 좌표 반경으로만 잡힌 마커의 측점값 라벨/검색 제외
|
||||
|
||||
**소요 시간**: 약 8분
|
||||
**Context 사용량**: input ~420k / output ~28k tokens
|
||||
|
||||
## 결론
|
||||
역 마커가 좌표 반경(pxPassesTo)으로만 잡혀 그 위치에 실제 측점값이 존재하지 않는 경우,
|
||||
**측점값 라벨을 숨기고 검색 대상에서도 제외**. 마커(동그라미)는 유지.
|
||||
|
||||
## 판정 (kmExists)
|
||||
- 각 통과의 실제 측점 = 그 통과 프레임의 연속 chain(p.km, 좌표탐지) / 측점기준 탐지는 targetM(=stationKmVal).
|
||||
- 표시 측점값 = stationKmVal(역 좌표 투영).
|
||||
- `kmExists = stationKmVal==null || |p.km - stationKmVal| <= stationTolerance(기본 20m)`
|
||||
- 측점기준 통과 → 일치 → true
|
||||
- 좌표 반경만으로 잡힌 통과(측점 동떨어짐) → false
|
||||
|
||||
## 변경 내용
|
||||
- `client/src/stationbar/StationBar.tsx`
|
||||
- `StructMark`에 `kmExists?: boolean` 추가.
|
||||
- structureMarks 두 경로(storeStructures / POI 폴백) push 시 kmExists 계산.
|
||||
- handleJumpToMileage 마커 후보 필터에 `m.kmExists !== false` 추가(값 없는 마커로는 검색 점프 안 함).
|
||||
- `client/src/stationbar/components/Timeline/Timeline.tsx`
|
||||
- 측점값 라벨 표시 루프에서 `s.kmExists === false`면 라벨 생략(동그라미·이름 라벨은 유지).
|
||||
|
||||
## 검증
|
||||
- `tsc + vite build` 통과(node 20). client/dist 재빌드(12:27).
|
||||
|
||||
## 참고
|
||||
- 측점기준 통과는 km=targetM=stationKmVal 로 동일 → 항상 kmExists=true(정상 측점은 영향 없음).
|
||||
- 마커 동그라미/브래킷/역명 라벨은 그대로. 숨기는 것은 '측점값(10m)' 라벨과 그 값으로의 검색뿐.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 55000 포트 외부(LAN) 접속 활성화
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~450k / output ~30k tokens
|
||||
|
||||
## 결론
|
||||
PM2 ghiVideo(55000) 를 같은 네트워크의 다른 기기에서 접속 가능하게 설정.
|
||||
접속 주소: **http://172.16.40.179:55000/** (Windows 이더넷 LAN IP).
|
||||
|
||||
## 점검 결과
|
||||
- 서버: `0.0.0.0:55000` 바인딩 ✓, CORS `origin:true`(모든 origin 허용) ✓ → 서버측 준비 완료.
|
||||
- 차단 원인: WSL2 기본 NAT 모드 → WSL IP(172.21.50.250)는 LAN 직접 도달 불가.
|
||||
|
||||
## 조치 (Windows 관리자/UAC)
|
||||
- netsh portproxy 추가: `0.0.0.0:55000 → 172.21.50.250:55000`
|
||||
(기존에 8080/3000/5173 등 동일 패턴 사용 중이라 55000만 추가)
|
||||
- 방화벽 인바운드 허용 규칙 "WSL ghiVideo 55000" (TCP 55000) 추가.
|
||||
- 스크립트: `C:\Windows\Temp\wsl_port_55000.cmd` (UAC 로 실행).
|
||||
|
||||
## 검증
|
||||
- portproxy 목록에 55000 등록 확인.
|
||||
- 방화벽 규칙 Enabled/Inbound/Allow 확인.
|
||||
- Windows→`http://localhost:55000/` Invoke-WebRequest → **200**.
|
||||
|
||||
## 주의 / 후속
|
||||
- WSL IP 는 재부팅/`wsl --shutdown` 시 변경 → portproxy(고정 172.21.50.250) 끊김. 재적용 필요.
|
||||
(요청 시 WSL IP 자동감지 재설정 스크립트 작성 가능)
|
||||
- 172.16.x 사내망 한정. 인터넷 접속은 공유기/회사 방화벽 포트포워딩 추가 필요.
|
||||
@@ -0,0 +1,302 @@
|
||||
# 드론 자세 평활/보간 + 영상 오버레이 렌더링 파이프라인
|
||||
|
||||
> 후임자 인계 + 특허 문서용 상세 기술 문서
|
||||
> 대상 소스: [client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) (1639줄), 투영 유틸 [client/src/utils/geoProjection.ts](../../client/src/utils/geoProjection.ts)
|
||||
|
||||
---
|
||||
|
||||
## 1. 기술 개요 / 목적
|
||||
|
||||
드론으로 촬영한 철도 선로 주행영상 위에, 측점(스테이션)·선로구조물(교량/터널/구교)·POI·선로 중심선·드론 GPS 궤적·나침반을 **영상 픽셀에 정확히 정합(AR 오버레이)** 시켜 표출하는 렌더링 파이프라인이다.
|
||||
|
||||
입력 데이터는 두 종류로, (a) 프레임별 드론 자세/위치 로그(SRT 추출: `frame, lat, lon, altitude, yaw, pitch, roll`)와 (b) 지리정보(측점·중심선·POI·구조물의 위경도+표고)이다. 핵심 변환은 핀홀 카메라 모델 기반의 월드(EPSG:5186 TM ENU) → 카메라 → 정규화 픽셀 투영([geoProjection.ts#L301](../../client/src/utils/geoProjection.ts#L301), [#L126](../../client/src/utils/geoProjection.ts#L126))이다.
|
||||
|
||||
이 문서가 다루는 본질적 과제는 **두 가지 노이즈/지연 문제**이다.
|
||||
|
||||
1. **자세/GPS 노이즈** — 원시 SRT 로그는 떨림이 있어 그대로 투영하면 라벨이 매 프레임 진동한다. 그러나 단순 평균은 회전 구간을 뭉개 라벨이 늦게 따라온다(지연). → **에지 보존 적응형 평활(smoothFrame)** 로 해결.
|
||||
2. **렌더 부드러움 vs 계산 비용** — 라벨 가시성/겹침 판정은 비싸고(전 프레임 × 전 항목), 60fps 매끄러운 이동은 매 프레임 재투영을 요구한다. → **가시성은 사전계산(precompute), 좌표는 RAF에서 연속포즈로 재투영**하는 2단 분리로 해결.
|
||||
|
||||
---
|
||||
|
||||
## 2. 핵심 알고리즘
|
||||
|
||||
### 2.1 에지(회전경계) 보존 적응형 이동평균 — `smoothFrame`
|
||||
|
||||
[StationOverlay.tsx#L577](../../client/src/components/overlay/StationOverlay.tsx#L577)
|
||||
|
||||
원시 드론 프레임 배열에서 인덱스 `i`를 중심으로 좌우 최대 `halfWin`(기본 60프레임 ≈ 2초)까지 이동평균을 취하되, **중심 프레임의 yaw와 차이가 임계 `YAW_EDGE = 8°` 이내인 인접 프레임까지만 창을 확장**한다.
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:577
|
||||
const smoothFrame = useCallback((frames: DroneFrameBasic[], i: number, halfWin: number): DroneFrameBasic => {
|
||||
let lo = i, hi = i;
|
||||
if (frames.length > 1 && halfWin > 0) {
|
||||
const YAW_EDGE = 8; // deg — 중심 대비 이 이상 벌어지면 다른(회전) 구간으로 보고 평균 제외
|
||||
const yc = frames[i].yaw;
|
||||
const near = (idx: number) => Math.abs(((frames[idx].yaw - yc + 540) % 360) - 180) <= YAW_EDGE;
|
||||
const loMin = Math.max(0, i - halfWin);
|
||||
const hiMax = Math.min(frames.length - 1, i + halfWin);
|
||||
while (lo > loMin && near(lo - 1)) lo--; // 회전경계에서 멈춤
|
||||
while (hi < hiMax && near(hi + 1)) hi++;
|
||||
}
|
||||
```
|
||||
|
||||
**수식.** 창 `[lo, hi]`(요소 수 `n = hi − lo + 1`)에 대해 lat/lon/altitude/pitch/roll은 산술평균. yaw는 각도이므로 단순 산술평균 시 0°/360° 경계에서 깨진다 → **원형평균(circular mean)**:
|
||||
|
||||
```
|
||||
yaw̄ = atan2( (1/n)·Σ sin(yawₖ), (1/n)·Σ cos(yawₖ) )
|
||||
```
|
||||
|
||||
[StationOverlay.tsx#L590](../../client/src/components/overlay/StationOverlay.tsx#L590):
|
||||
|
||||
```typescript
|
||||
const yr = f.yaw * Math.PI / 180;
|
||||
sinYaw += Math.sin(yr); cosYaw += Math.cos(yr);
|
||||
// ...
|
||||
yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI,
|
||||
```
|
||||
|
||||
`near()`의 `((Δ + 540) % 360) − 180`은 yaw 차를 항상 `[−180°, 180°]`로 정규화하는 최단각 차이 패턴이다.
|
||||
|
||||
**의도 / 트레이드오프.**
|
||||
- 직선 주행: yaw가 거의 일정 → 창이 풀폭(±60fr)으로 펴져 GPS/자세 노이즈를 강하게 억제.
|
||||
- 회전 중: 회전경계에서 창이 좁아져(한쪽 또는 양쪽이 중심 근처에서 멈춤) **실제 자세를 즉시 추종(지연 없음)**.
|
||||
- 회전 종료 후: 회전 프레임을 배제한 채 창이 **한 프레임씩** 점진적으로 재확장 → 튐 없음.
|
||||
- 트레이드오프: `YAW_EDGE`가 너무 작으면 완만한 곡선도 회전으로 보아 노이즈 억제가 약해지고, 너무 크면 회전이 평균에 섞여 지연. 8°는 떨림(보통 <2°)과 실제 회전을 가르는 경험값.
|
||||
- (주석에 기록된) 과거 '변화율 기반 창 축소' 방식은 회전 종료 시 창이 한꺼번에 재확장되며 회전 프레임이 다시 섞여 한 번 튀는 부작용이 있어 경계보존 방식으로 교체됨 ([StationOverlay.tsx#L569](../../client/src/components/overlay/StationOverlay.tsx#L569) 주석).
|
||||
|
||||
### 2.2 연속 프레임번호 보간 포즈 — `poseAt`
|
||||
|
||||
[StationOverlay.tsx#L670](../../client/src/components/overlay/StationOverlay.tsx#L670)
|
||||
|
||||
영상의 부드러운 재생시간(60fps 벽시계 보간, §4.1)에서 산출된 **실수(實數) 프레임번호 `estFrame`** 에 대응하는 자세를 만든다. 이산 SRT 프레임만으로는 30fps 격자에 갇혀 라벨이 계단식으로 움직이므로, 양 이웃 프레임의 **평활 포즈(smoothFrame 결과)** 를 선형보간한다.
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:670 — 이진탐색으로 estFrame을 끼우는 두 프레임 찾기
|
||||
let lo = 0, hi = arr.length - 1;
|
||||
while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].frame < estFrame) lo = m + 1; else hi = m; }
|
||||
const i2 = lo, i1 = Math.max(0, lo - 1);
|
||||
const f1 = arr[i1].frame, f2 = arr[i2].frame;
|
||||
const frac = f2 > f1 ? Math.min(1, Math.max(0, (estFrame - f1) / (f2 - f1))) : 0;
|
||||
const a = smoothFrame(arr, i1, h), b = smoothFrame(arr, i2, h);
|
||||
const L = (x: number, y: number) => x + (y - x) * frac;
|
||||
let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180; // 최단각
|
||||
return { ...a, frame: estFrame,
|
||||
lat: L(a.lat, b.lat), lon: L(a.lon, b.lon), altitude: L(a.altitude, b.altitude),
|
||||
pitch: L(a.pitch, b.pitch), roll: L(a.roll, b.roll), yaw: a.yaw + dy * frac };
|
||||
```
|
||||
|
||||
**수식.** 보간계수 `frac = (estFrame − f1)/(f2 − f1)`. 위치/피치/롤은 `L(x,y) = x + (y−x)·frac`. yaw는 359°→1° 같은 경계에서 긴 쪽으로 도는 것을 막기 위해 **최단각 보간**: `Δ = ((b.yaw − a.yaw + 540) % 360) − 180`, `yaw = a.yaw + Δ·frac`.
|
||||
|
||||
**트레이드오프.** O(log n) 이진탐색으로 호출당 비용이 낮아 매 RAF 프레임 호출 가능. smoothFrame을 매 프레임 두 번 호출하므로(`a`, `b`) 평활창이 매우 클 때는 비용이 늘 수 있으나, 실데이터 규모에서는 무시 가능.
|
||||
|
||||
### 2.3 속도적응 EMA + 이상치 거부 — `smoothStep` (One-Euro 방식)
|
||||
|
||||
[StationOverlay.tsx#L48](../../client/src/components/overlay/StationOverlay.tsx#L48)
|
||||
|
||||
`poseAt`로 재투영한 화면 정규좌표를 매 프레임 그대로 쓰면 잔여 떨림이 보인다. 화면단에서 One-Euro 필터 사상(idea)을 적용: **느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종**.
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:48
|
||||
function smoothStep(prev, tx, ty, maxAlpha, minAlpha, speedRef): DispPos {
|
||||
if (!prev) return { x: tx, y: ty, rej: 0, vx: 0, vy: 0 };
|
||||
const dx = tx - prev.x, dy = ty - prev.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES) {
|
||||
return { x: prev.x, y: prev.y, rej: prev.rej + 1, vx: prev.vx, vy: prev.vy }; // 이상치: 위치 유지
|
||||
}
|
||||
// 속도 벡터 평활 → 떨림(왕복)은 상쇄돼 작게, 실제 이동은 유지.
|
||||
const vx = prev.vx + (dx - prev.vx) * SMOOTH_VEL_BETA;
|
||||
const vy = prev.vy + (dy - prev.vy) * SMOOTH_VEL_BETA;
|
||||
const speed = Math.hypot(vx, vy);
|
||||
const a = Math.min(maxAlpha, minAlpha + (maxAlpha - minAlpha) * Math.min(1, speed / Math.max(1e-4, speedRef)));
|
||||
return { x: prev.x + dx * a, y: prev.y + dy * a, rej: 0, vx, vy };
|
||||
}
|
||||
```
|
||||
|
||||
**수식.**
|
||||
- 이상치 거부: 프레임당 점프 `d = ‖(dx,dy)‖`가 `REJECT_DIST = 0.12`(정규화 화면거리)를 넘으면 갱신을 버리고 이전 위치 유지. 단 연속 거부가 `MAX_REJECT_FRAMES = 8`을 넘으면(=시크/재등장 같은 진짜 큰 이동) 수용 → 영구 고착 방지.
|
||||
- 속도 평활: `v ← v + (Δ − v)·β`, `β = SMOOTH_VEL_BETA = 0.25`. 떨림은 매 프레임 방향이 번갈아(왕복) `v`가 상쇄돼 ≈0, 실제 이동은 방향이 일관돼 `v` 유지(저크 제거).
|
||||
- 적응 alpha: `α = min(maxAlpha, minAlpha + (maxAlpha − minAlpha)·min(1, speed/speedRef))`.
|
||||
- 정지/떨림(`speed≈0`) → `α≈minAlpha`(기본 0.12, 강한 평활).
|
||||
- 빠른 팬(`speed ≥ speedRef`, 기본 `speedRef=0.010`) → `α=maxAlpha`(기본 `emaAlpha=1.0`, 즉시 추종).
|
||||
- 위치 갱신: `pos ← pos + Δ·α`.
|
||||
|
||||
**의도 / 트레이드오프.** 입력이 이미 smoothFrame(±60fr) + poseAt 보간으로 한 차례 평활됐으므로 화면 EMA의 `maxAlpha`는 가볍게 둔다(주석: 과거 고정 α=0.01은 지연 ~1.65s로 라벨이 늦게 따라옴). `minAlpha`↓ / `speedRef`↑ 일수록 1배속 떨림은 줄지만 미세 추종 지연이 늘어난다. 이 세 값은 패널에서 데이터셋별로 실시간 튜닝·저장된다([StationOverlay.tsx#L494](../../client/src/components/overlay/StationOverlay.tsx#L494)).
|
||||
|
||||
추가로 **픽셀 히스테리시스**가 EMA 위에 한 겹 더 적용된다: 화면 px 위치가 0.75px 이상 움직일 때만 정수 좌표를 갱신해 저속에서 반올림 깜빡임을 제거한다([StationOverlay.tsx#L1048](../../client/src/components/overlay/StationOverlay.tsx#L1048), [#L1084](../../client/src/components/overlay/StationOverlay.tsx#L1084)).
|
||||
|
||||
---
|
||||
|
||||
## 3. 라벨 사전계산 + 겹침 억제
|
||||
|
||||
### 3.1 `startLabelPrecompute` — requestIdleCallback 청크 처리, 월드좌표 저장
|
||||
|
||||
[StationOverlay.tsx#L692](../../client/src/components/overlay/StationOverlay.tsx#L692)
|
||||
|
||||
데이터 로드 완료 시(또는 params/평활/범위 변경 시 500ms debounce, [#L824](../../client/src/components/overlay/StationOverlay.tsx#L824)) 전 프레임에 대해 **어떤 라벨이 보이는가(가시집합)와 겹침 해소 결과**를 미리 계산해 `Map<frameNum, LabelCache>`에 적재한다. 핵심 설계는 **화면좌표가 아니라 월드좌표(lat/lon/z)를 저장**한다는 점이다([LabelCache 정의 #L123](../../client/src/components/overlay/StationOverlay.tsx#L123)). 가시성/겹침은 한 번만 결정하고, 실제 화면 위치는 RAF가 매 프레임 연속포즈로 재투영한다.
|
||||
|
||||
**청크 처리.** 메인스레드 블로킹을 피하려 `requestIdleCallback`으로 한 번에 `CHUNK = 200`프레임씩 처리한다.
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:717
|
||||
const step = () => {
|
||||
if (precomputeIdRef.current !== id) return; // 취소됨 (새 계산이 시작되면 id 불일치 → 중단)
|
||||
const end = Math.min(processed + CHUNK, total);
|
||||
while (processed < end) {
|
||||
const idx = (startIdx + processed) % total; // startIdx→끝→0→… 순환
|
||||
processed++;
|
||||
const drone = smoothFrame(frames, idx, SMOOTH_HALF);
|
||||
// ... 측점/POI 가시판정 + 겹침억제 → newMap.set(drone.frame, {...})
|
||||
}
|
||||
if (!published) { labelMapRef.current = newMap; published = true; } // 첫 청크 후 즉시 노출
|
||||
if (processed < total) requestIdleCallback(step, { timeout: 200 });
|
||||
};
|
||||
step(); // 첫 청크는 동기 실행 → 정지 중에도 토글/재계산 즉시 반영
|
||||
```
|
||||
|
||||
세 가지 영리한 설계가 있다.
|
||||
- **현재 프레임부터 순환 처리** (`startIdx = currentFrameIdxRef.current`, `idx = (startIdx + processed) % total`): 지금 화면에 보이는 구간을 먼저 계산해 재생 즉시 라벨이 뜬다.
|
||||
- **재계산 시 옛 맵 복사로 시작** ([#L696](../../client/src/components/overlay/StationOverlay.tsx#L696)): 빈 구간 없이 새 청크가 덮어쓰며 갱신.
|
||||
- **취소 토큰** `precomputeIdRef`: 새 계산이 시작되면 `id`가 증가해 진행 중이던 step이 자동 중단(중복 작업 방지).
|
||||
|
||||
가시판정은 카메라 클리핑(`Zc < CLIP_Z = 0.1` 제외 = 카메라 뒤/너무 가까움), 화면 밖(`pxRaw/pyRaw`가 [−0.02, 1.02] 밖) 제외, POI는 수평직선거리 `distH > MAX_RANGE` 제외로 이뤄진다([#L731](../../client/src/components/overlay/StationOverlay.tsx#L731), [#L748](../../client/src/components/overlay/StationOverlay.tsx#L748)).
|
||||
|
||||
### 3.2 겹침 억제 / 우선순위 / 진행방향 변형
|
||||
|
||||
[StationOverlay.tsx#L755](../../client/src/components/overlay/StationOverlay.tsx#L755)
|
||||
|
||||
화면상 가까이 겹치는 라벨을 솎아낸다. 임계: 가로 `POI_MERGE_X = 0.10`, 세로 `POI_MERGE_Y = 0.035`(정규화). 비등방(가로 관대, 세로 엄격)은 텍스트 라벨이 가로로 길기 때문.
|
||||
|
||||
**우선순위 정렬**(겹침 시 누가 남는가): ① 철도역/역사(`railRank=0`)를 최우선, ② 같은 등급 내에서는 드론에 가까운 것(`dist` 작은 것)을 우선.
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:757
|
||||
const railRank = (cat: string): number => (cat === '철도역' || cat === '역사' ? 0 : 1);
|
||||
poiCand.sort((a, b) => railRank(a.category) - railRank(b.category) || a.dist - b.dist);
|
||||
const dirTag = routeDirRef.current === '상' ? '(상)' : routeDirRef.current === '하' ? '(하)' : '';
|
||||
```
|
||||
|
||||
**진행방향 변형 우선**: 같은 구조물이 상/하행 형제로 두 개 있을 때(예: `회덕제1가도교(상)` / `회덕제1가도교(하)`), 드론이 실제 지나는 선로 방향(routeMeta의 direction, [#L447](../../client/src/components/overlay/StationOverlay.tsx#L447))의 변형을 남긴다. `baseStruct()`로 괄호 변형을 떼 형제임을 판정하고, 이미 수용된 라벨이 비-방향태그인데 새 후보가 방향태그면 교체한다([#L777](../../client/src/components/overlay/StationOverlay.tsx#L777)):
|
||||
|
||||
```typescript
|
||||
const k = accepted[overlapIdx];
|
||||
if (dirTag && baseStruct(c.title) === baseStruct(k.title) && c.title.includes(dirTag) && !k.title.includes(dirTag)) {
|
||||
accepted[overlapIdx] = c;
|
||||
}
|
||||
```
|
||||
|
||||
겹침제외 토글 OFF면 억제 없이 전부 표시하며, 동일좌표 라벨은 `labelRow / labelRowCount`로 세로 행 배치된다([#L784](../../client/src/components/overlay/StationOverlay.tsx#L784))(렌더 시 `ROW_H = 24px`로 중앙정렬, [#L1101](../../client/src/components/overlay/StationOverlay.tsx#L1101)).
|
||||
|
||||
---
|
||||
|
||||
## 4. RAF 렌더 루프 (계산 없이 조회 + 투영만)
|
||||
|
||||
[StationOverlay.tsx#L906](../../client/src/components/overlay/StationOverlay.tsx#L906)
|
||||
|
||||
매 프레임 `requestAnimationFrame`에서 캔버스를 다시 그린다. **무거운 계산(가시성/겹침)은 하지 않고**, precompute가 채운 가시집합을 조회 + 연속포즈로 재투영 + 그리기만 한다.
|
||||
|
||||
### 4.1 연속 시간 → 보간 포즈
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:941
|
||||
const estTime = timeRef
|
||||
? timeRef.current
|
||||
: currentTimeSecRef.current + (performance.now() - timeUpdateWallRef.current) / 1000;
|
||||
const estFrame = estTime * VIDEO_FPS; // VIDEO_FPS = 30000/1001 ≈ 29.97
|
||||
const dronePose = poseAt(estFrame) ?? (...폴백 smoothFrame...);
|
||||
const lines = buildLines(dronePose);
|
||||
```
|
||||
|
||||
`timeRef`는 VideoPlayer의 `smoothTimeRef`로, 매 RAF마다 단조(monotonic) 벽시계 보간으로 갱신되는 재생시간이다([VideoPlayer.tsx#L84](../../client/src/components/player/VideoPlayer.tsx#L84)). 일시정지·시크·배속을 모두 보정하므로 라벨/선/궤적이 영상과 동기된 채 60fps로 매끄럽게 흐른다.
|
||||
|
||||
### 4.2 object-fit:cover 정규좌표 → 화면 변환 (`coverRef`)
|
||||
|
||||
영상은 컨테이너를 비율유지·크롭하며 채우므로(CSS `object-fit:cover`), 오버레이도 같은 cover 사각형에 매핑해야 정합된다.
|
||||
|
||||
```typescript
|
||||
// StationOverlay.tsx:923
|
||||
const { w: vW, h: vH } = videoSizeRef.current;
|
||||
let dispW = W, dispH = H, offX = 0, offY = 0;
|
||||
if (vW > 0 && vH > 0) {
|
||||
const s = Math.max(W / vW, H / vH); // cover: 더 큰 배율(넘침=크롭)
|
||||
dispW = vW * s; dispH = vH * s;
|
||||
offX = (W - dispW) / 2; offY = (H - dispH) / 2; // 음수 = 화면 밖(크롭)
|
||||
}
|
||||
coverRef.current = { offX, offY, dispW, dispH, W, H };
|
||||
const vx = (nx: number) => offX + nx * dispW; // 정규(0~1, 영상프레임) → 화면 px
|
||||
const vy = (ny: number) => offY + ny * dispH;
|
||||
```
|
||||
|
||||
`coverRef`는 RAF가 매 프레임 갱신하고, 포인터 핸들러(클릭→정규좌표 역변환 `clientToVideoNorm`, [#L1211](../../client/src/components/overlay/StationOverlay.tsx#L1211))와 자동팝업 동기화도 공유한다.
|
||||
|
||||
### 4.3 중심선 / 드론궤적 투영 (`buildLines`)
|
||||
|
||||
[StationOverlay.tsx#L606](../../client/src/components/overlay/StationOverlay.tsx#L606). 보간포즈를 받아 매 프레임 직접 투영한다(캐시 미사용 → 라인이 라벨처럼 부드럽게 이동).
|
||||
- **근거리 클리핑**: 세그먼트 양 끝의 `Zc`가 `CLIP_Z=0.1` 경계를 가로지르면 교점 `t = (CLIP_Z − z1)/(z2 − z1)`로 잘라 투영([#L622](../../client/src/components/overlay/StationOverlay.tsx#L622)) → 카메라 뒤 점이 화면 반대편으로 튀는 것 방지.
|
||||
- **화면밖 컬링**: 비트마스크 `oc()` outcode로 양 끝이 같은 변 밖이면 세그먼트 폐기([#L612](../../client/src/components/overlay/StationOverlay.tsx#L612)).
|
||||
- 드론궤적: GPS 경로(lat/lon)를 표고 `Z`에 투영, `PW=15` 폭 이동평균으로 평활, `STEP=3` 샘플, `MAX_FWD=200m` 앞까지만([#L646](../../client/src/components/overlay/StationOverlay.tsx#L646)).
|
||||
|
||||
라벨 자체도 캐시의 월드좌표를 `dronePose`로 재투영 → `smoothStep` → 히스테리시스 후 그린다([#L1039](../../client/src/components/overlay/StationOverlay.tsx#L1039) 측점, [#L1071](../../client/src/components/overlay/StationOverlay.tsx#L1071) POI).
|
||||
|
||||
### 4.4 나침반 미니맵 (heading-up)
|
||||
|
||||
[StationOverlay.tsx#L954](../../client/src/components/overlay/StationOverlay.tsx#L954). 본 포즈는 ±60fr 평활이라 회전 시 나침반이 지연된다 → **별도로 ±3fr 가벼운 평활 yaw**를 보간해 회전에 즉시 반응. `--rot` CSS 변수로 DOM 회전을 갱신하며, **360° 누적 언랩**으로 359°→0° 점프를 방지한다:
|
||||
|
||||
```typescript
|
||||
const target = -(rawYaw + paramsRef.current.yawOffset);
|
||||
const delta = (((target - minimapRotRef.current) % 360) + 540) % 360 - 180;
|
||||
minimapRotRef.current += delta;
|
||||
minimapRef.current.style.setProperty('--rot', `${minimapRotRef.current}deg`);
|
||||
```
|
||||
|
||||
### 4.5 자동 컴팩트 팝업 동기화 (setInterval 150ms)
|
||||
|
||||
[StationOverlay.tsx#L376](../../client/src/components/overlay/StationOverlay.tsx#L376). 가시 구조물(`visStructRef`, RAF가 매 프레임 채움)마다 라벨 옆에 선명한 DOM 팝업(시설종별/구조형식/연장 등 컴팩트 3필드)을 자동 생성/제거한다. **60fps RAF에서 setState를 남발하면 리렌더 폭주**하므로, 가시집합 변화 감지를 **150ms 인터벌**로 분리해 추가/제거가 있을 때만 `setInfoPopups`를 호출한다. 팝업의 실제 화면 위치는 다시 RAF가 라벨 표시좌표를 따라 매 프레임 갱신(EMA `PA=0.18`, 화면하단 근접 시 위로 플립, [#L1146](../../client/src/components/overlay/StationOverlay.tsx#L1146)).
|
||||
|
||||
---
|
||||
|
||||
## 5. 성능 설계 — 60fps 유지 전략
|
||||
|
||||
| 작업 | 비용 | 배치 | 빈도 |
|
||||
|------|------|------|------|
|
||||
| 가시성/겹침 판정 (전 프레임 × 전 항목) | 높음 | precompute | 1회(+설정변경 시 debounce 재계산), idle 청크 200fr |
|
||||
| 좌표 투영 (보간포즈 → 화면) | 중간 | RAF | 매 프레임 |
|
||||
| 평활/EMA/히스테리시스 | 낮음 | RAF | 매 프레임 |
|
||||
| 팝업 가시집합 setState | 리렌더 유발 | setInterval 150ms | 변화 시에만 |
|
||||
|
||||
**핵심 = precompute(가시성)와 RAF(좌표) 분리.** "어떤 라벨을 그릴지"(비싼 결정)는 영상 변화와 무관하게 한 번 계산해 두고, "어디에 그릴지"(가벼운 투영)만 매 프레임 한다. 이로써 RAF 루프는 Map 조회 + 행렬 투영 + 캔버스 드로잉만 남아 60fps를 유지한다([파일 상단 주석 #L1](../../client/src/components/overlay/StationOverlay.tsx#L1)).
|
||||
|
||||
부가 전략: 사전계산의 idle 청크 처리(메인스레드 비블로킹) + 취소토큰, 현재 프레임 우선 순환계산, 첫 청크 즉시노출, 픽셀 히스테리시스(불필요한 재드로 억제), 팝업 위치 EMA(물결/잔상 방지), 데이터셋별 보정값 localStorage 영속화([#L490](../../client/src/components/overlay/StationOverlay.tsx#L490)).
|
||||
|
||||
---
|
||||
|
||||
## 6. 한계 / 업그레이드 포인트
|
||||
|
||||
- **단일 캐시 프레임키 의존 잔존**: 라벨 가시집합은 `Math.floor(estFrame)`로 정수 프레임의 캐시를 조회([#L1022](../../client/src/components/overlay/StationOverlay.tsx#L1022)). 좌표는 연속이지만 가시집합 자체는 프레임 경계에서 바뀐다 → 빠른 배속에서 라벨 등장/퇴장이 미세하게 튈 수 있음.
|
||||
- **중심선 per-frame 투영 비용**: `buildLines`가 전 중심선 점을 매 프레임 투영. 점 수가 많아지면 부담 → 화면 근방 구간만 투영하는 공간색인(앞서 컬링 전 prefilter)으로 개선 여지.
|
||||
- **POI 표고 부정확**: POI는 실측 표고가 없어 최근접 선로표고 또는 '드론고도 − N'으로 가정([#L744](../../client/src/components/overlay/StationOverlay.tsx#L744)) → 드래그/DEM override로 수동보정. DEM 자동조회 통합이 업그레이드 포인트.
|
||||
- **VFR/프레임 누락**: `VIDEO_FPS` 상수(29.97) 가정. 가변프레임율 영상은 frame↔time 매핑 오차 발생.
|
||||
- **평활 파라미터 수동 튜닝**: YAW_EDGE/REJECT_DIST/speedRef 등이 상수 또는 수동슬라이더 → 데이터셋 통계 기반 자동 캘리브레이션 가능.
|
||||
- **단일 스레드**: precompute가 메인스레드 idle 의존. Web Worker로 옮기면 대용량에서 더 안정.
|
||||
|
||||
---
|
||||
|
||||
## 7. 특허 관점 신규성
|
||||
|
||||
1. **에지(회전경계) 보존 적응형 이동평균** — yaw 차 임계로 평균창을 회전경계에서 멈추고, 회전 종료 후 한 프레임씩 점진 재확장하여 *직선구간 노이즈 억제*와 *회전구간 무지연 추종*을 동시에 달성. 회전 프레임 재혼입에 의한 1회 튐을 구조적으로 제거(변화율 기반 방식 대비).
|
||||
|
||||
2. **월드좌표 사전계산 + 연속포즈 재투영의 2단 분리** — 라벨 가시성/겹침/우선순위(비싼 결정)는 월드좌표로 1회 사전계산하고, 화면 위치(가벼운 투영)는 매 RAF 연속 보간포즈로 재계산. "무엇을 그릴지"와 "어디에 그릴지"의 분리로 60fps AR 오버레이를 단일스레드에서 달성.
|
||||
|
||||
3. **연속 프레임번호 보간 포즈 + 최단각 yaw** — 단조 벽시계 보간 재생시간을 실수 프레임번호로 변환, 이산 SRT 자세를 평활-후-보간하여 이산격자(30fps)를 넘는 60fps 연속 자세를 생성. yaw는 원형평균(평활)·최단각(보간)으로 360° 경계 안정.
|
||||
|
||||
4. **속도적응 화면 평활(One-Euro 사상) + 연속거부 한계** — 속도벡터 평활로 떨림(왕복상쇄)과 실이동(방향일관)을 분리해 alpha를 적응시키고, 이상치 거부에 *연속거부 한계*를 두어 시크/재등장 시 영구고착을 방지.
|
||||
|
||||
5. **진행방향 인지 형제 라벨 해소** — 상/하행 형제 구조물이 화면상 겹칠 때 노선 진행방향((상)/(하)) 변형을 우선 표시. 철도역 최우선 + 근접거리 차선의 다단 우선순위와 결합.
|
||||
|
||||
6. **object-fit:cover 정합 + 양방향 좌표 변환** — 영상 크롭 사각형에 오버레이를 일치시키는 정규좌표 변환과 그 역변환(클릭→월드 역투영)으로, 영상 위 라벨 클릭/드래그 보정(focal 역산·표고 역산·지면교차)을 동일 좌표계에서 처리.
|
||||
|
||||
---
|
||||
|
||||
**소요 시간**: 18분
|
||||
**Context 사용량**: input 88k / output 12k tokens
|
||||
@@ -0,0 +1,32 @@
|
||||
# GhiVideo 종합 기술 명세서 작성 (인계+특허 기초)
|
||||
|
||||
**소요 시간**: 약 35분
|
||||
**Context 사용량**: input ~600k / output ~36k tokens
|
||||
|
||||
## 결론
|
||||
사용된 전 기술을 소스 인용과 함께 1개 종합 문서로 정리. 후임 인계 + 특허 출원 기초자료 겸용.
|
||||
md/html/pdf 3종 생성(PDF 18p, 한글 임베드).
|
||||
|
||||
## 산출물
|
||||
- `docs/기술명세_GhiVideo_종합기술문서.md` (베이스)
|
||||
- `docs/기술명세_GhiVideo_종합기술문서.html` (자체완결)
|
||||
- `docs/기술명세_GhiVideo_종합기술문서.pdf` (A4 18p, Malgun 임베드, 페이지번호)
|
||||
|
||||
## 방법
|
||||
- 5개 서브시스템을 병렬 서브에이전트로 정밀 조사 후 종합:
|
||||
1) 카메라 투영·FOV 정합(geoProjection.ts) 2) 드론 자세 평활·오버레이 렌더(StationOverlay.tsx)
|
||||
3) 측점·스테이션바(chainage.ts/StationBar/Timeline) 4) 백엔드·데이터·상태(server/geoData/store)
|
||||
5) 기존 특허/발명 문서 + history 타임라인
|
||||
- 투영 파이프라인은 정본 1개(§2)로 통합, 지오이드 N=25.8m 통일, 특허/인계 섹션 통합.
|
||||
|
||||
## 문서 구성 (13장)
|
||||
개요·아키텍처 / 좌표·투영 정본 / 카메라·FOV / 측점표고정합(발명B) / 자세평활·렌더 /
|
||||
측점·스테이션바 / 위치보정 역투영(발명C) / 백엔드·데이터 / 개발타임라인 / 특허종합 /
|
||||
검증결과표 / 후임자 인계 / 한계·출원전 확인.
|
||||
|
||||
## 검증
|
||||
- scripts/md2docs.sh 로 html+pdf 생성, %PDF-1.7 18페이지 /FontFile(Malgun) 임베드 확인.
|
||||
|
||||
## 참고
|
||||
- 특허 출원 시 §3~§7·§10 + history 원문(특히 2026-06-22 POI투영오차)을 변리사에 함께 제공 권장.
|
||||
- KIPRIS 전문검색은 미수행 — 출원 전 필수.
|
||||
@@ -0,0 +1,35 @@
|
||||
# 드론 흔들림 잡기 + 글자 붙이기 — 초등학생용 쉬운 설명 작성
|
||||
|
||||
## 작업 내용
|
||||
|
||||
기존 기술문서 [2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md](2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md)의 내용이 실제 코드에 적용되어 있는지 검증한 뒤, 그 기술을 **초등학생 눈높이 + SVG 그림**으로 쉽게 풀어쓴 문서를 신규 작성.
|
||||
|
||||
### 1. 기술문서 ↔ 실제 코드 적용 여부 검증
|
||||
- [StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx)(1639줄)에서 문서가 인용한 함수/상수 위치를 grep으로 대조
|
||||
- `smoothFrame`(L577), `poseAt`(L670), `smoothStep`(L48), `startLabelPrecompute`(L692), `buildLines`(L606), `coverRef`(L257), `VIDEO_FPS=30000/1001`(L25), `YAW_EDGE=8`(L580), `REJECT_DIST=0.12`, `MAX_REJECT_FRAMES=8`, `SMOOTH_VEL_BETA=0.25` 모두 실제 존재·일치 확인
|
||||
- 결론: 기술문서는 가상이 아니라 실제 구현 코드를 정확히 기술한 것
|
||||
|
||||
### 2. 초등학생용 쉬운설명 문서 작성
|
||||
- 신규 파일: [docs/쉬운설명_드론흔들림잡기와_글자붙이기.md](../쉬운설명_드론흔들림잡기와_글자붙이기.md)
|
||||
- 기존 `쉬운설명_GhiVideo_기술이야기.md`의 SVG figure 스타일을 따름
|
||||
- 8개 SVG 삽화 포함, 비유 중심 서술 (도시락/만화 사이그림/거북이토끼 등)
|
||||
- 다룬 핵심 기술 4+1가지:
|
||||
1. 에지보존 적응형 평활(smoothFrame) → "곧을 땐 많이, 돌 땐 멈춰서"
|
||||
2. 연속프레임 보간(poseAt) → "30장을 60장처럼 사이 채우기"
|
||||
3. 속도적응 EMA + 이상치거부(smoothStep) → "느리면 살살, 빠르면 빨리"
|
||||
4. precompute + RAF 2단 분리 → "미리 정하기 vs 그때그때 그리기 (도시락 비유)"
|
||||
5. 겹침 우선순위 + 상/하행 형제 라벨 + 나침반(보너스)
|
||||
|
||||
### 3. HTML/PDF 변환
|
||||
- `scripts/md2docs.sh`로 변환 완료
|
||||
- 산출물: `쉬운설명_드론흔들림잡기와_글자붙이기.{html,pdf}` (PDF 118KB)
|
||||
|
||||
## 산출물
|
||||
- `docs/쉬운설명_드론흔들림잡기와_글자붙이기.md` (22KB)
|
||||
- `docs/쉬운설명_드론흔들림잡기와_글자붙이기.html` (28KB)
|
||||
- `docs/쉬운설명_드론흔들림잡기와_글자붙이기.pdf` (118KB)
|
||||
|
||||
---
|
||||
|
||||
**소요 시간**: 12분
|
||||
**Context 사용량**: input 95k / output 11k tokens
|
||||
@@ -0,0 +1,39 @@
|
||||
# 지도 POI → 영상 매핑 기술 — 초등학생용 쉬운 설명 작성
|
||||
|
||||
## 작업 내용
|
||||
|
||||
"지도상의 POI 좌표를 영상에 매핑한 기술들"을 실제 코드에서 모두 조사·나열하고, 초등학생 눈높이 + SVG 그림으로 설명하는 문서를 신규 작성.
|
||||
|
||||
### 1. 실제 구현 기술 조사
|
||||
- [geoProjection.ts](../../client/src/utils/geoProjection.ts) 전체 정독 + [StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx)의 POI 처리부 분석
|
||||
- 매핑 파이프라인을 구성하는 12개 기술 식별:
|
||||
1. 좌표계 변환 (proj4, EPSG:4326→5186 한국 TM)
|
||||
2. ENU 월드 3D 좌표 (East-North-Up)
|
||||
3. 지오이드 높이 보정 (정표고↔타원체고, 대전≈25.8m)
|
||||
4. 자세 회전 행렬 (yaw/pitch/roll → R_b2w/R_w2c)
|
||||
5. 상대 위치 벡터 (대상−드론, offX/Y/Z)
|
||||
6. 핀홀 카메라 원근투영 (Xc/Zc·f/sensor)
|
||||
7. 화각(초점거리 24mm·센서 36×20.25)
|
||||
8. 카메라 뒤 클리핑 (Zc<CLIP_Z)
|
||||
9. POI 표고 추정 (최근접 중심선 nearestCL / 드론−24m)
|
||||
10. 거리 필터 (수평거리 distH<MAX_RANGE 1000m, 앞/옆 fwd/side)
|
||||
11. 역투영 보정 (worldFromPixel/solveZForPixelY/groundPointFromPixel + focal 역산)
|
||||
12. object-fit:cover 정규좌표→화면 변환
|
||||
|
||||
### 2. 쉬운설명 문서 작성
|
||||
- 신규 파일: [docs/쉬운설명_지도POI를_영상에_붙이는기술.md](../쉬운설명_지도POI를_영상에_붙이는기술.md)
|
||||
- 기존 쉬운설명 문서 스타일 계승, SVG 삽화 14개 + 비유 중심 서술
|
||||
- 기술 한눈에 보기 표 → 각 기술별 그림 상세 → 전체 요약 → 특허포인트 → 용어사전
|
||||
|
||||
### 3. HTML/PDF 변환
|
||||
- `scripts/md2docs.sh`로 변환 완료 (PDF 137KB)
|
||||
|
||||
## 산출물
|
||||
- `docs/쉬운설명_지도POI를_영상에_붙이는기술.md` (33KB)
|
||||
- `docs/쉬운설명_지도POI를_영상에_붙이는기술.html` (41KB)
|
||||
- `docs/쉬운설명_지도POI를_영상에_붙이는기술.pdf` (137KB)
|
||||
|
||||
---
|
||||
|
||||
**소요 시간**: 15분
|
||||
**Context 사용량**: input 130k / output 18k tokens
|
||||
@@ -0,0 +1,20 @@
|
||||
# DefVideo → GhiVideo 업그레이드 문서화
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~70k / output ~9k tokens
|
||||
|
||||
## 작업 내용
|
||||
- `b23042/DefVideo`(구버전)와 `b23042/GhiVideo`(신버전) 소스 트리를 `diff -r`로 전체 비교
|
||||
- 두 저장소는 git 히스토리 분리(공통 커밋 없음) → 워킹 트리 직접 비교로 차이 분석
|
||||
- 변경 규모 정량화(파일별 +/− 라인 수, 신규 파일 줄수)
|
||||
- 주요 변경 파일 3그룹을 병렬 Explore 에이전트로 상세 분석
|
||||
- StationOverlay/Minimap/RoutePanel
|
||||
- geoData/geoProjection/chainage/geo타입/geoStore
|
||||
- StationBar/Timeline/VideoPlayer/settingsStore/elevation
|
||||
|
||||
## 결론
|
||||
- 서버·공유타입은 거의 동일(elevation API 라우트만 신규), 클라이언트는 약 +3,000줄 순증으로 대폭 발전
|
||||
- 핵심 3축: ① V2.0 building/ 폴더 CSV 형식 대응 ② 투영 정확도 개선(지오이드/DEM/역투영) ③ 영상 오버레이·편집 UI 고도화
|
||||
|
||||
## 산출물
|
||||
- `docs/DefVideo→GhiVideo_업그레이드_상세.md` (상세 업그레이드 문서)
|
||||
@@ -0,0 +1,42 @@
|
||||
# KMZ 필수화 — building POI/구조물 CSV 폴백 제거 + 누락 경고
|
||||
|
||||
## 배경 / 결정
|
||||
- 정책: **KMZ는 항상 필수**. 없으면 "데이터 누락"으로 보고 재구축해서 KMZ를 전달받음.
|
||||
- 기존 코드는 KMZ 없으면 **조용히 building CSV로 폴백** → 불완전 데이터가 에러 없이 통과(정책과 상충).
|
||||
- 사용자 선택: **"경고만 표시"** (앱은 계속 동작, CSV POI/구조물 폴백 제거, 측점은 유지).
|
||||
|
||||
## 변경 내용
|
||||
|
||||
### 1. geoData.ts — 죽은 파서 제거 + KMZ 단일 소스화
|
||||
- 삭제: `parsePois`, `parseStructures`, `parseHistoricStations`, `parseAccessDoors` + 전용 헬퍼 `rowProps`, `cleanHeader`
|
||||
- `loadFolderGeoData`:
|
||||
- `Promise.all`에서 위 4개 파서 호출 제거(헛수고 파싱 제거)
|
||||
- `useKmz` 분기 → `kmzMissing` 판정으로 교체: `!kmz || (pois==0 && structures==0)`
|
||||
- KMZ 누락 시 `console.warn`, 정상 시 `console.log`
|
||||
- `pois = kmz?.pois ?? []`, `baseStructures = kmz?.structures ?? []`
|
||||
- `structures`에서 `historicStations` concat 제거 → **역사 중복 위험 제거**(역사는 KMZ KAKAO_RAIL에서만 생성)
|
||||
- 반환값에 `kmzMissing` 추가
|
||||
- 유지: `parseStations`(측점), `parseDroneFrames`, `parseKmz`, `mergeStructures`, `parseRouteMeta`, `parsePoiOverrides`, `buildCenterlineFromStations` 및 공용 헬퍼(`makeFieldIndexer`/`cell`/`readCsv`/`findBuildingFile`/`mmssToSeconds` 등)
|
||||
|
||||
### 2. 타입/스토어 (kmzMissing 전파)
|
||||
- [types/geo.ts](../../client/src/types/geo.ts): `FolderGeoData.kmzMissing: boolean` 추가
|
||||
- [store/geoStore.ts](../../client/src/store/geoStore.ts): `GeoStore.kmzMissing` + `EMPTY` 기본값 + `loadFromFolder`에서 set
|
||||
|
||||
### 3. UI 경고
|
||||
- [VideoPlayer.tsx](../../client/src/components/player/VideoPlayer.tsx) `handleSelectFolder`: 로드 후 `kmzMissing && (stations||frames 존재)` 면 `alert`로 "KMZ 누락 → 재구축 필요(측점·드론은 정상)" 안내
|
||||
|
||||
### 4. 문서 갱신
|
||||
- [docs/구현상세_GhiVideo_기술-소스코드매칭.md](../구현상세_GhiVideo_기술-소스코드매칭.md): 데이터 흐름도 + 소스 우선순위 콜아웃 + 파서 표를 KMZ 필수 정책으로 갱신(폴백 제거 명시)
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` (node v20.20.2): **통과** (시스템 기본 node v12라 nvm node20 사용)
|
||||
- `npm run build` (tsc && vite build): **성공** (219 modules, 빌드 1.49s)
|
||||
|
||||
## 비고
|
||||
- 측점.csv(루트)는 KMZ에 없어 그대로 필수 — 빠지면 스테이션바/체이니지/중심선 동작 불가.
|
||||
- 빌드 산출물 반영됨 → 55000 PM2 서빙 시 재시작/재배포 필요할 수 있음.
|
||||
|
||||
---
|
||||
|
||||
**소요 시간**: 25분
|
||||
**Context 사용량**: input 175k / output 22k tokens
|
||||
@@ -0,0 +1,22 @@
|
||||
# 구현 상세 — 기술·소스코드 매칭 문서화
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~120k / output ~16k tokens
|
||||
|
||||
## 작업 내용
|
||||
- 앞서 설명한 기술들을 실제 소스코드(파일·라인·핵심 코드)와 1:1 매칭한 구현 상세 문서 작성
|
||||
- 영역별 3개 Explore 에이전트로 정확한 함수 위치/라인번호/코드 스니펫 추출
|
||||
- 오버레이/미니맵/RoutePanel
|
||||
- geoData/geoProjection/chainage/타입/geoStore
|
||||
- StationBar/Timeline/VideoPlayer/settingsStore/elevation
|
||||
- 추출 결과를 14개 섹션 + 빠른 색인표로 종합
|
||||
|
||||
## 산출물
|
||||
- `docs/구현상세_GhiVideo_기술-소스코드매칭.md`
|
||||
- 전체 데이터 흐름도 → 파싱 → 투영/역투영 → 체이니지 → 오버레이 렌더 → 라벨평활 → 겹침/팝업 → 위치보정(드래그/DEM/세로화각) → 미니맵 → 측점바/이동거리축 → smoothTimeRef → 상태관리 → 서버 고도 API
|
||||
- 기능↔소스 빠른 색인표(파일:라인) 포함
|
||||
- 각 기능마다 핵심 코드 스니펫 + 동작 원리 설명
|
||||
|
||||
## 참고
|
||||
- 라인번호는 2026-06-30 기준이며 코드 수정 시 달라질 수 있음
|
||||
- 기존 문서 `DefVideo→GhiVideo_업그레이드_상세.md`, `쉬운설명_GhiVideo_기술이야기.md`와 상호 참조
|
||||
@@ -0,0 +1,20 @@
|
||||
# 발표 문서 작성 — 좌표 정합과 측점 기반 구현
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~150k / output ~20k tokens
|
||||
|
||||
## 작업 내용
|
||||
- 개발 내용을 발표용 슬라이드형 마크다운으로 작성
|
||||
- 주제: 드론 GPS·영상·POI 좌표를 영상 위 정확한 위치에 배치하는 기술 + 프레임→측점 기반 전환
|
||||
- 기존 소스 분석(geoProjection/chainage/StationBar/StationOverlay/VideoPlayer) 결과를 재활용해 그림·도식 중심으로 구성
|
||||
|
||||
## 산출물
|
||||
- `docs/발표_GhiVideo_좌표정합과측점기반구현.md`
|
||||
- 9개 슬라이드 섹션 + 부록 소스 색인
|
||||
- 핵심 ①공간(4단계 투영+지오이드) ②시간(smoothTimeRef) ③안정(One Euro) ④프레임→측점 전환
|
||||
- ASCII 도식: 투영 파이프라인, 핀홀 카메라, ENU, 측점 투영, 이동거리축(호버), 측점바
|
||||
- 프레임 vs 측점 비교표, 핵심 소스 색인표
|
||||
|
||||
## 참고
|
||||
- `---` 슬라이드 구분으로 Marp/reveal.js 변환 가능
|
||||
- 상세 구현 문서(`구현상세_...md`)와 상호 참조
|
||||
@@ -0,0 +1,37 @@
|
||||
# 재생 중 데이터 폴더 교체 시 "media could not be loaded" 수정
|
||||
|
||||
## 증상
|
||||
재생 중에 다른 데이터 폴더를 드래그&드롭하면 지오 데이터(측점/POI/스테이션바)는 정상 로드되는데
|
||||
**영상만** 상단에 "The media could not be loaded …" 에러가 뜨고 검은 화면.
|
||||
|
||||
## 원인 — objectURL 조기 해제 race
|
||||
[useVideoPlayer.loadLocalFile](../../client/src/hooks/useVideoPlayer.ts#L72)이 blob URL 해제를 `player.one('emptied', …)` 이벤트에 의존.
|
||||
소스 교체 시:
|
||||
1. 첫 로드 → `.one('emptied', revoke url1)` 등록
|
||||
2. 드롭 → `createObjectURL`로 **url2** 생성 → `player.src(url2)` (이때 `'emptied'`가 **비동기 예약**) → 직후 `.one('emptied', revoke url2)` 등록
|
||||
3. 예약된 `'emptied'` 발화 → url1·url2 핸들러가 **동시 실행** → 방금 만든 **url2까지 revoke** → 소스 무효 → 로드 실패
|
||||
|
||||
첫 로드는 이전 'emptied'가 없어 정상, **교체 시에만** 재현.
|
||||
|
||||
## 수정 — ref 기반 결정적 해제
|
||||
- `objectUrlRef`로 현재 blob URL 추적, `'emptied'` 핸들러 제거
|
||||
- `loadLocalFile`: 새 `src` 설정 후 **이전 URL만** revoke (새 URL은 보존)
|
||||
- `loadServerStream`: 로컬→서버 전환 시 이전 blob URL revoke
|
||||
- 언마운트(dispose) cleanup: 남은 blob URL revoke
|
||||
- 부가: 교체 직전 재생 중이었으면(=드롭 교체) 새 영상도 `player.play()`로 이어서 재생
|
||||
(drop은 사용자 제스처라 자동재생 정책 통과. 최초 로드는 `prevUrl` 없음 → 기존처럼 자동재생 안 함)
|
||||
|
||||
## 파일
|
||||
- [client/src/hooks/useVideoPlayer.ts](../../client/src/hooks/useVideoPlayer.ts)
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit`: 통과 / `npm run build`: 성공 (node v20.20.2)
|
||||
- 재현/회귀: 재생 중 폴더 드롭 → 새 영상 로드+재생, 콘솔 에러 없음(코드 경로상). 실브라우저 확인은 하드 새로고침 후 폴더 재드롭 필요.
|
||||
|
||||
## 반영 방법
|
||||
- client만 변경 → **하드 새로고침(Ctrl+Shift+R)** 후 폴더 드롭으로 확인. PM2 재시작 불필요.
|
||||
|
||||
---
|
||||
|
||||
**소요 시간**: 12분
|
||||
**Context 사용량**: input 198k / output 26k tokens
|
||||
@@ -0,0 +1,24 @@
|
||||
# 발표자료 SVG 도식화 전면 개편
|
||||
|
||||
## 작업 내용
|
||||
기존 [발표_GhiVideo_좌표정합과측점기반구현.md](../발표_GhiVideo_좌표정합과측점기반구현.md)가 ASCII 아트 도식이라 가독성이 낮아, **인라인 SVG 그림 16개로 전면 교체**하고 색상 체계·표로 도식화해 이해도를 높임.
|
||||
|
||||
## 변경점
|
||||
- ASCII 다이어그램 → **SVG figure 16개** (기존 쉬운설명 문서들의 `figure.fig` 스타일 계승)
|
||||
- 색상 의미 통일: 🟦공간(투영, 파랑) · 🟩시간(초록) · 🟧안정(주황) · 🟦측점(청록) · 드론(보라)
|
||||
- 주요 그림: 목표 화면 목업, 입력3종 결합, 전체 파이프라인(레인), 4단계 투영, ENU·회전·핀홀·지오이드, 시간보간(sparse↔dense), One Euro 3케이스, 시간축 한계, GPS→측점 투영, 이동거리축(호버), 측점바 목업, 역투영 보정, 최종 정리도
|
||||
- 내용/코드근거/소스색인 표는 유지(정확도 보존), 슬라이드 `---` 구분 유지(Marp 호환)
|
||||
- `<style>` 블록 추가(figure 스타일 + lead 콜아웃)
|
||||
|
||||
## 산출물
|
||||
- `docs/발표_GhiVideo_좌표정합과측점기반구현.md` (41KB)
|
||||
- `.html` (59KB) / `.pdf` (181KB) — md2docs.sh 변환
|
||||
|
||||
## 비고
|
||||
- weasyprint 경고 `user-select: none`(31행)은 무시됨(렌더 영향 없음).
|
||||
- 원본 ASCII 버전은 덮어씀(내용은 보존+개선).
|
||||
|
||||
---
|
||||
|
||||
**소요 시간**: 18분
|
||||
**Context 사용량**: input 215k / output 32k tokens
|
||||
@@ -0,0 +1,103 @@
|
||||
# POI 라벨 사라진 뒤 팝업 재등장 수정
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~80k / output ~3k tokens
|
||||
|
||||
## 문제
|
||||
POI 라벨이 화면 밖으로 사라졌는데, 방금 전 위치에 POI 팝업이 다시 뜨는 문제.
|
||||
|
||||
## 원인
|
||||
[client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) 의
|
||||
마커 그리기 루프(1114행)가 라벨이 화면(비디오 크롭) 밖으로 나가도 `cc.Zc >= CLIP_Z` 이기만 하면
|
||||
매 프레임 `visStructRef` 에 계속 등록.
|
||||
- 팝업 제거 로직(RAF): 라벨이 off-container 상태 4프레임이면 팝업 제거.
|
||||
- 자동 sync 인터벌(150ms): `vis.has(title)` 이 아직 true → 팝업을 즉시 다시 추가.
|
||||
→ 둘이 싸워 팝업이 마지막 위치에 되살아남.
|
||||
|
||||
## 수정
|
||||
`visStructRef.set` 을 팝업 off 판정과 **동일한 화면 범위**(`px/py ∈ [-16, W+16]/[-16, H+16]`)로 제한.
|
||||
`vx(nx)=offX+nx*dispW` 는 팝업 off-check 의 `sx=offX+disp.x*dispW` 와 같은 좌표계라 일치.
|
||||
→ 라벨이 화면 밖이면 vis 에서도 빠져 sync 가 되살리지 않음(제거 로직과 일관).
|
||||
|
||||
## 추가 수정 (잔여 1프레임 번쩍임)
|
||||
`visStructRef` 가드만으론 sync 인터벌이 팝업 DOM 을 **새로 mount** 하는 순간의 번쩍임이 남음:
|
||||
새 팝업 요소는 React 초기 스타일(`top: sy+27`)로 **보이는 상태**로 붙고, 다음 RAF 가
|
||||
'화면 밖→숨김' 판정을 내리기 전 한 프레임 stale 위치에 번쩍임.
|
||||
|
||||
→ 팝업 ref 콜백에서 `popupPosRef`(RAF 위치 확정 시 기록)에 아직 없는 '신규' 팝업은
|
||||
`visibility:hidden` 으로 mount. RAF 가 온스크린 확인 후에만 visible 로 전환(off 면 계속 숨김→제거).
|
||||
visibility 는 RAF 명령형으로만 관리(style prop 미포함)라 재렌더에도 유지됨.
|
||||
|
||||
## 추가 수정 2 (hidden-mount 가드 무력화 — 진짜 원인)
|
||||
hidden-mount 가드는 `!popupPosRef.has(id)` 일 때만 숨긴다. 그런데 팝업 제거 경로가 여럿인데
|
||||
보조 맵(popupPosRef/popupMissRef) 정리는 **RAF off-4프레임 경로만** 했음.
|
||||
→ sync `toRemoveIds` / 수동(✕·ESC·빈곳클릭) 제거 시 `popupPosRef` 잔존 → 같은 title 재추가 시
|
||||
`popupPosRef.has=true` → 숨김 안 됨 → React 초기 스타일(`top: sy+27`)로 **보이는 채 mount** →
|
||||
라벨 없이 팝업만 stale 위치에 나타남(+ RAF 가 stale 위치에서 EMA 로 미끄러짐).
|
||||
|
||||
수정:
|
||||
1. `infoPopups` 변경 effect 에서 사라진 id 의 보조 맵 일괄 정리(단일 출처).
|
||||
2. sync `toRemoveIds` 처리 시 즉시 `popupPosRef`/`popupMissRef` 정리(재추가 전에 확실히 비움).
|
||||
|
||||
## 추가 수정 3 (definitive — 라벨 가시성과 완전 결합)
|
||||
증상이 계속 동일해 근본 재분석: 팝업 off 판정이 라벨 **십자마커 중심** ±16px 슬랙 + 화면 안 **clamp**
|
||||
조합이라, 마커가 경계 근처(예: 오른쪽 끝 밖 ~16px)면 라벨 텍스트는 화면 밖(안 보임)인데
|
||||
팝업만 clamp 되어 화면 안에 뜸 → "라벨 없이 팝업만".
|
||||
|
||||
수정: 이번 프레임에 라벨이 **실제로 화면 안에 그려졌는지**(히트박스∩뷰포트)를 `onScreenLabels`
|
||||
집합으로 만들고, 팝업 표시/visStruct 등록을 이 집합에만 의존하도록 통일.
|
||||
- `off = !disp || !onScreenLabels.has(id)` (좌표 슬랙·clamp 기반 판정 제거)
|
||||
- visStruct 등록도 `poiVisible`(히트박스 온스크린) 기준
|
||||
|
||||
## 추가 수정 4 (사라졌다 잠깐 재등장 → 완전 제거)
|
||||
증상: 라벨 사라짐 → 팝업 사라짐 → 잠깐 재등장 → 사라짐. 원인은 sync(150ms 추가) ↔ RAF(제거) 충돌.
|
||||
- 경계에서 라벨이 1~2프레임 깜빡이면 visStruct 재등록 → sync 가 팝업 재생성 → RAF 가 다시 제거.
|
||||
|
||||
수정:
|
||||
1. 추가 히스테리시스: 라벨이 **연속 ADD_STREAK(6프레임 ~100ms)** 이상 화면 안일 때만 visStruct 등록
|
||||
(poiOnScreenStreakRef). 짧은 깜빡임으론 팝업 생성 안 됨.
|
||||
2. 제거 단일화: sync 의 팝업 제거 로직 삭제 → **RAF(onScreenLabels + 4프레임 유예)가 유일한 제거 권한**.
|
||||
add/remove 충돌 제거 → 라벨 사라지면 팝업도 사라진 뒤 재생성 안 됨.
|
||||
|
||||
## 추가 수정 5 (진짜 리그레션 원인 — flip 왕복)
|
||||
사용자 지적: 이 증상은 '하단 겹침' 수정(추가수정 없음, 첫 위치 수정) 이후 생긴 리그레션.
|
||||
→ 가시성 문제가 아니라 flip(아래↔위 전환) 로직에 **히스테리시스가 없어서** 발생.
|
||||
라벨이 하단 flip 임계값 부근이면 드론 흔들림/평활로 `sy` 가 미세하게 오르내릴 때마다
|
||||
`ty+ph > H-2` 가 참↔거짓 왕복 → 팝업이 아래↔위로 **순간이동(bigJump 스냅)** → "사라졌다 나타났다".
|
||||
|
||||
수정: `popupFlipRef`(위/아래 상태) + 히스테리시스(HYST=48px). '위'로 간 뒤엔 아래에 48px 이상
|
||||
여유가 생겨야만 '아래'로 복귀 → 경계 왕복 제거. flip 상태도 제거 경로에서 정리.
|
||||
|
||||
## 추가 수정 6 (하단 이탈 즉시 제거 — 유예 삭제)
|
||||
요구: 라벨이 하단으로 벗어나면 팝업 즉시 사라지고 재등장 금지.
|
||||
잔여 원인: 제거에 4프레임 '유예'가 있어, 그 사이 흔들림으로 라벨이 경계로 잠깐 되들어오면
|
||||
RAF 가 아직 안 지워진 팝업을 다시 visible 로 바꿈 → "사라졌다 잠깐 나타났다".
|
||||
|
||||
수정: off(라벨 화면 밖) 판정 시 유예 없이 **즉시 toRemove**(+숨김). 재등장은 sync 의
|
||||
ADD_STREAK(6프레임 연속 안정) 게이트가 막음 → 짧은 복귀로는 재생성 안 됨.
|
||||
mid-screen 은 라벨 히트박스가 가장자리에서 멀어 off 가 안 나므로 영향 없음.
|
||||
|
||||
## 추가 수정 7 (하단 기준 = 영상 영역(스테이션바 위), 모니터 아님)
|
||||
요구: 팝업이 스테이션바에 걸치지 않게, 라벨이 바 밑으로 내려가면 팝업 제거. 기준은 영상 재생 영역.
|
||||
- 스테이션바는 영상 위 z-20 오버레이(absolute bottom-0), 팝업은 z-40 → 바 위에 걸쳐 보였음.
|
||||
- 유효 하단 `HB = H − barHeight` 도입(barHeightRef, ResizeObserver 실측 ~130px).
|
||||
- `labelOnScreen` 하단 기준 `H-2` → `HB-2`: 라벨 히트박스가 바 밑이면 off → 즉시 제거.
|
||||
- 팝업 flip/clamp 하단 기준도 `H` → `HB`: 팝업이 바 위로만 배치(걸침 방지).
|
||||
|
||||
## 추가 수정 8 (flip 제거 + 바 뒤로 슬라이드)
|
||||
요구: 팝업이 라벨 위로 flip하면 안 됨. 항상 라벨 아래에 있다가 라벨이 바 밑으로 사라질 때 같이 사라짐.
|
||||
- flip/clamp 둘 다 문제였음(flip=위로 튐, clamp=바에 걸쳐 멈춤).
|
||||
|
||||
수정:
|
||||
1. flip·하단 clamp 제거 → 팝업 `ty = sy + LABEL_HALF + GAP` (항상 라벨 아래, 자유 하강).
|
||||
2. 팝업들을 클립 컨테이너로 감쌈: `absolute left-0 right-0 top-0 overflow-hidden` + `bottom: barHeight`
|
||||
(높이 = 영상영역 HB). 팝업이 라벨 따라 내려가면 바 영역에서 클립돼 '바 뒤로 슬라이드'되어 사라짐.
|
||||
3. 제거는 label off(HB 기준) 그대로 → 라벨이 바 밑이면 팝업 제거.
|
||||
|
||||
## 배포 확인
|
||||
- :55000 서빙 index.html → `assets/index-yfJyCKp1.js` (이번 빌드 해시와 일치)
|
||||
- ecosystem: ghiVideo, cwd=프로젝트루트, clientDist=client/dist (정적 즉시 반영)
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과
|
||||
- `npm run build` 성공 → :55000 프로덕션 반영 (브라우저 강력 새로고침 필요)
|
||||
@@ -0,0 +1,23 @@
|
||||
# POI 팝업 ↔ 라벨 겹침 수정
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~60k / output ~4k tokens
|
||||
|
||||
## 문제
|
||||
POI 팝업이 화면 아래쪽에 다가올수록 라벨(십자마커+글자)과 팝업창이 겹치는 문제.
|
||||
|
||||
## 원인
|
||||
[client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) 의 RAF 팝업 위치 계산에서
|
||||
여백 `GAP=16` 을 라벨 **중심**(`sy`, 십자마커 위치) 기준으로 잡음.
|
||||
라벨 글자·아이콘이 `sy` 위아래로 ~13px 차지 → 실제 여백은 3px뿐. 하단 위로 플립해도 동일 3px라 계속 겹침.
|
||||
|
||||
## 수정
|
||||
- `LABEL_HALF=15`(라벨 반높이) 도입 → 라벨 **바깥 가장자리** 기준으로 띄움.
|
||||
- 아래: `ty = sy + LABEL_HALF + GAP`
|
||||
- 위 플립: `aboveTy = sy - LABEL_HALF - GAP - ph`
|
||||
- `GAP` 16→12 (실제 여백 3px → 12px)
|
||||
- 초기 렌더 top `sy+16 → sy+27` 로 맞춰 1프레임 튐 방지.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과
|
||||
- `npm run build` 성공 → :55000 프로덕션 반영
|
||||
@@ -0,0 +1,27 @@
|
||||
# 역 마커 — 실제 측점 일치 기준으로 정정 (162080 누락 수정)
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~230k / output ~4k tokens
|
||||
|
||||
## 문제
|
||||
직전 수정(kmExists 게이트)이 너무 세서, **진짜 162080 위치의 대전조차장 마커까지 사라짐**.
|
||||
|
||||
## 원인
|
||||
kmExists 게이트가 `|p.km − stationKmVal|` 비교인데:
|
||||
- 탐지는 `s.station`(등록 측점 162080) → p.km = 162080
|
||||
- stationKmVal = `projectChainage(역 좌표)` (좌표 투영) — 등록값과 어긋날 수 있음
|
||||
→ 둘이 다르면 게이트가 실제 162080 통과까지 제거.
|
||||
|
||||
## 수정 ([StationBar.tsx](../../client/src/stationbar/StationBar.tsx))
|
||||
게이트 방식 폐기, 탐지 소스로 구분:
|
||||
1. 역사(역)는 **좌표근접(pxPassesTo) 폴백을 배제** → `pxPassesAtMileage`(실제 측점 일치)로만 탐지.
|
||||
- `if (!passes.length && cat !== '역사' && ...) pxPassesTo(...)` (교량/터널만 허용)
|
||||
2. `if (!kmExists) continue` 게이트 제거(주 경로 + 폴백 경로). kmExists 는 라벨/검색 참고용으로만 유지.
|
||||
|
||||
## 효과
|
||||
- ① 좌표만 가까운(측점 다른) 조차장 재진입(드론 162210인데 162080)은 안 찍힘.
|
||||
- ② 진짜 162080 통과에는 정상 표시.
|
||||
- 검색은 이미 chain 기준(별도 수정)이라 마커 변경과 무관하게 정확.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, `npm run build` 성공 → 해시 `index-Cdsf6M1x.js`(HUD `· 검색v3`) :55000 서빙.
|
||||
@@ -0,0 +1,26 @@
|
||||
# 역(스테이션) 마커 — 직교 투영 측점 일치 시에만 표시
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~180k / output ~4k tokens
|
||||
|
||||
## 문제
|
||||
스테이션바에서 드론이 대전조차장(역 측점 162k080) '좌표에만' 가까우면, 실제 드론 측점이
|
||||
162k290(210m 밖)이어도 대전조차장역 마커가 찍혔다. → 직교 위치가 안 맞는데 역 표시됨.
|
||||
|
||||
## 원인
|
||||
[StationBar.tsx](../../client/src/stationbar/StationBar.tsx) 구조물 마크 생성:
|
||||
- 역사 '보강 탐지'가 `pxPassesTo`(좌표 근접) 통과를 합집합으로 추가 → 측점 불일치 통과까지 마커.
|
||||
- `kmExists`(=드론 측점 p.km 와 역 측점 stationKmVal 차이 ≤ tol)가 false여도 마커(동그라미)는 유지했음.
|
||||
|
||||
## 수정
|
||||
1. 역사 좌표근접 '보강 탐지' 블록 제거 (측점 불일치 재진입을 억지로 추가하던 부분).
|
||||
2. 마커 push 시 `if (!kmExists) continue;` 게이트 추가(주 경로 + 폴백 경로 both).
|
||||
→ 역은 **드론의 직교 투영 측점이 역 측점과 stationTolerance(기본 20m) 이내일 때만** 표시.
|
||||
→ 교량/터널은 stationKmVal=null → kmExists 항상 true → 영향 없음.
|
||||
|
||||
## 효과
|
||||
- 대전조차장에서 드론이 162k290일 때 162k080 역 마커가 더 이상 안 뜸.
|
||||
- 측점 일치 통과(pxPassesAtMileage)는 그대로 유지 → 정상 통과·재진입은 계속 표시.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, `npm run build` 성공 → 새 해시 `index-CJflcoyT.js` :55000 서빙 확인.
|
||||
@@ -0,0 +1,34 @@
|
||||
# 영상 fps 데이터 기반 자동 산출 (고정 29.97 → 가변)
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~150k / output ~6k tokens
|
||||
|
||||
## 배경
|
||||
프레임번호↔시간 변환이 `VIDEO_FPS = 30000/1001`(29.97)로 하드코딩 → 30fps 영상 전용.
|
||||
24/60fps 등 다른 영상은 이름표가 어긋남. 어떤 fps 영상이 로드될지 미리 알 수 없음.
|
||||
|
||||
## 문제 데이터
|
||||
드론 CSV(`frame_cnt,latitude,...`)엔 **시간이 없고 frame_cnt(프레임 번호)만** 있음.
|
||||
→ fps 를 CSV 단독으로 못 구함. **영상 길이(duration)** 와 결합 필요.
|
||||
|
||||
## 해결
|
||||
`effectiveFps = maxFrameCnt / duration` → 표준 fps(23.976/24/25/29.97/30/50/59.94/60) 중
|
||||
±10% 이내면 스냅, 아니면 원시값. 데이터/길이 미확보 시 29.97 폴백.
|
||||
|
||||
### 변경 파일 (2개, 소규모)
|
||||
- [VideoPlayer.tsx](../../client/src/components/player/VideoPlayer.tsx)
|
||||
- `effectiveFps` useMemo 신설(storeFrames+duration). `VIDEO_FPS` 하드코딩 4곳 제거
|
||||
(frame 계산 / HUD 표시 / 프레임입력 이동 / StationOverlay prop).
|
||||
- useFrameStep 의 fps(VFC 감지, 31 오감지)는 미사용 → destructure 제거.
|
||||
- [StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx)
|
||||
- `fpsRef`(prop fps 기반) 추가. 프레임↔시간 변환 VIDEO_FPS 사용처(최근접 프레임 탐색, estFrame)를
|
||||
fpsRef.current 로 교체. VIDEO_FPS 상수는 폴백 기본값으로만 유지.
|
||||
|
||||
## 주의/전제
|
||||
- 드론 CSV 가 영상 전체 구간을 덮는다는 전제(마지막 frame_cnt ≈ 영상 끝). 부분만 덮으면 fps 과소 →
|
||||
스냅 실패 시 원시값 사용. 필요 시 메타데이터/FFprobe 로 보강 가능.
|
||||
- 표준 목록 밖 fps 는 스냅 없이 원시값 사용.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과
|
||||
- `npm run build` 성공 → 새 해시 `index-BbUe4Gr4.js` :55000 서빙 확인
|
||||
@@ -0,0 +1,33 @@
|
||||
# 종점(대전조차장) 마커 끝-이동 제거 → 실제 직교 위치에 그대로 표시
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~260k / output ~5k tokens
|
||||
|
||||
## 사용자 원칙
|
||||
- POI(영상 오버레이) 정보: 현재 위치 반경 안만 표시.
|
||||
- **스테이션바 항목: 반경 없이, 측점선과 직교로 만나는 시설물이면 무조건 실제 위치에 표시.**
|
||||
|
||||
## 문제
|
||||
검색이 찾은 162080 자리에 대전조차장 마커가 없음.
|
||||
원인: `placeUnreachedTerminal` 이 endGapPx>0 일 때 **최우측 역사 마커를 트랙 끝(TRACK_END)** 으로
|
||||
이동('미도착' 표시) → 드론이 종점 야드에서 162080을 여러 번 지나는데 마지막 통과 마커가 끝으로
|
||||
옮겨져 검색 위치(실제 162080)와 어긋남.
|
||||
|
||||
## 수정 ([StationBar.tsx](../../client/src/stationbar/StationBar.tsx))
|
||||
`placeUnreachedTerminal` 를 **no-op**(marks 그대로 반환)으로 변경 → 마커 끝-이동 제거.
|
||||
- 모든 162080 통과 마커가 실제 직교 위치에 유지(사용자 원칙과 일치).
|
||||
- 종점 이름(endStationName=대전조차장역)은 오른쪽 끝 고정 표시 유지.
|
||||
- 회색 미도착 gap(endGapPx) 자체는 트랙 색으로만 유지(마커는 안 옮김).
|
||||
|
||||
## 관련 상태
|
||||
- 역사: pxPassesAtMileage(측점 일치)로만 탐지(좌표근접·게이트 없음) → 직교 일치 통과에만, 반경 없음.
|
||||
- 교량/터널: pxPassesAtMileage 우선 + (측점/좌표 투영 없을 때만) pxPassesTo 폴백. 필요 시 폴백 제거 검토.
|
||||
|
||||
## 보정: 미도착 종점 마커는 '추가'로 복원
|
||||
no-op 로 두니 드론이 못 간 '종점 162080'의 대전조차장 마커까지 사라짐(사용자 지적).
|
||||
→ placeUnreachedTerminal 을 '이동'이 아니라 '추가'로 변경:
|
||||
- endGapPx>0 이면 최우측 역사 마커를 **복사**해 TRACK_END 에 unreached 로 push(기존 마커 유지).
|
||||
- 결과: 지나간 162080 통과는 실제 위치에 그대로 + 못 간 종점 162080은 끝에 미도착 표시.
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, `npm run build` 성공 → 해시 `index-D66Yy1Dn.js`(HUD `· 검색v5`) :55000 서빙.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 지도 기반 나침반(OSM, 노스업) 추가 + 타입 전환
|
||||
|
||||
**소요 시간**: 약 30분
|
||||
**Context 사용량**: input ~340k / output ~9k tokens
|
||||
|
||||
## 요구
|
||||
우측 상단 나침반을 2종 중 선택. 새 '지도 기반 나침반' = 나침반 안에 지도 + 실시간 위치.
|
||||
사용자 선택: **OSM 온라인 타일 · 노스업(북쪽 고정) · 화면표시 옵션에 전환 버튼.**
|
||||
|
||||
## 구현
|
||||
### 클라이언트
|
||||
- [store/settingsStore.ts](../../client/src/store/settingsStore.ts): `compassType: 'analog'|'map'` + setter (persist).
|
||||
- [components/overlay/MapCompass.tsx](../../client/src/components/overlay/MapCompass.tsx) (신규):
|
||||
- OSM 타일을 Web Mercator 로 배치, 원형 클립. 노스업(지도 고정) + 현재위치 중심 점 + 진행방향 화살표(yaw 회전) + 상단 N.
|
||||
- `poseRef`(부모 RAF 갱신 {lat,lon,yaw}) 를 내부 RAF 로 읽어: 화살표 즉시 회전 + 지도 팬(transform) + 임계 이동 시 타일 재배치(setState).
|
||||
- 타일 URL = **서버 프록시** `/api/tile/{z}/{x}/{y}.png` (외부 직접 로드는 CSP/COEP 로 차단됨).
|
||||
- [components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx):
|
||||
- `mapPoseRef` 추가, RAF 나침반 블록에서 headingDeg 항상 계산해 기록(아날로그 회전은 렌더 중일 때만).
|
||||
- 렌더: compassType 에 따라 `<MapCompass>` / `<Minimap>` 선택.
|
||||
- 화면표시 옵션 '표시 토글'에 나침반 [눈금|지도] 버튼 추가.
|
||||
|
||||
### 서버 (프록시 필수)
|
||||
- CSP `img-src 'self'` + COEP `require-corp` 라 외부 타일 `<img>` 직접 로드 불가(elevation.ts 와 동일 제약).
|
||||
- [server/src/routes/tile.ts](../../server/src/routes/tile.ts) (신규): `GET /api/tile/:z/:x/:y` →
|
||||
OSM 타일 fetch(식별 User-Agent) 후 같은 출처로 중계. `Cross-Origin-Resource-Policy: same-origin`, 7일 캐시, 좌표 검증(범위 밖 400).
|
||||
- [server/src/app.ts](../../server/src/app.ts): `app.use('/api/tile', tileRouter)`.
|
||||
|
||||
## 배포
|
||||
- server/client 빌드 + **PM2 restart ghiVideo**(서버 라우트 반영 필수) 완료.
|
||||
- 검증: `/api/tile/16/x/y.png` → 200 image/png, CORP same-origin, 7일 캐시. 범위 밖 → 400. 클라 `index-BS7bYMJ3.js`.
|
||||
|
||||
## 추가: 줌 + 호버 확대 + 휠 줌
|
||||
- settingsStore `compassZoom`(12~19, 기본16) + setter(persist). MapCompass 에서 project(z 동적) 사용.
|
||||
- 나침반 하단 −/z/+ 버튼(pointer-events auto). 줌 변경 시 현재위치 기준 타일 재배치.
|
||||
- **마우스 오버 → 2배 확대**(transform scale, 우상단 고정 origin, 140ms), **벗어나면 원복**.
|
||||
- **휠 줌**: 컨테이너 native wheel 리스너(passive:false) → deltaY 로 compassZoom ±1(위=확대).
|
||||
- 호버/휠 위해 컨테이너 pointer-events auto(해당 영역만 이벤트 캡처).
|
||||
|
||||
## 개선(사용자 피드백)
|
||||
- 확대: CSS scale(흐릿) → **위젯 지름 자체 확대**(BASE_D 150 → HOVER_D 300, 타일 재배치) → 더 넓은
|
||||
영역을 선명하게. 마우스 오버 시 확대, **벗어나면 기본 크기+기본 줌(16) 복귀**.
|
||||
- 방향 표시: 빨간 화살표 → **반투명 역삼각형(▽)** = 드론 시야(중심에서 촬영방향으로 벌어짐, yaw 회전).
|
||||
- **줌 +/− 버튼·z값 표시 삭제.** 줌은 휠만(오버 중), 벗어나면 기본값.
|
||||
- 상단 **N**: SVG text 로(위젯 크기에 맞춰 스케일) **크게·붉은색·반투명**.
|
||||
- settingsStore 의 compassZoom 제거(줌은 MapCompass 로컬 상태로 이동, 세션 임시).
|
||||
|
||||
## 추가: 위성지도 옵션
|
||||
- 타일 프록시를 소스별로 확장: [server/routes/tile.ts](../../server/src/routes/tile.ts) `GET /api/tile/:source/:z/:x/:y`
|
||||
- source=osm → OpenStreetMap 일반, source=sat → **Esri World Imagery 위성**(z/row/col 순서, jpeg).
|
||||
- settingsStore `compassMapStyle: 'street'|'sat'`(persist). MapCompass 타일 URL `/api/tile/${src}/...`.
|
||||
- 화면표시 옵션에 지도 [일반|위성] 토글(지도 타입일 때만 노출).
|
||||
- server 재빌드 + PM2 restart. 검증: osm→200 png, sat→200 jpeg.
|
||||
- 주의: Esri World Imagery 는 출처표기 필요(내부 도구, 저부하). 대량이면 라이선스 검토.
|
||||
|
||||
## 주의/추후
|
||||
- OSM 타일 사용 정책: 저부하 유지(줌16 고정 + 클라 96px 이동 시에만 재배치 + 서버 7일 캐시). 대량이면 자체 타일/유료 서비스 검토.
|
||||
- 인터넷 없으면 타일 미표시(테두리/점/화살표만). 오프라인 필요 시 '노선 개요도(데이터 기반)' 타입 추가 검토.
|
||||
@@ -0,0 +1,45 @@
|
||||
# 측점 검색 — 없는 측점 검색 시 엉뚱한 곳으로 튀는 문제 수정
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~200k / output ~3k tokens
|
||||
|
||||
## 문제
|
||||
스테이션바에서 `162080`(162k080) 검색 → 130m 떨어진 `162k210`으로 이동.
|
||||
|
||||
## 원인
|
||||
[StationBar.tsx `handleJumpToMileage`](../../client/src/stationbar/StationBar.tsx#L845) 검색 3단계:
|
||||
1. 마커(structureMarks) 표시측점 일치
|
||||
2. 연속 측점(chain)이 허용오차(stationTolerance 20m) 안
|
||||
3. **폴백: 둘 다 없으면 '무조건 가장 가까운 측점'으로 이동** ← 문제
|
||||
|
||||
162080이 이 영상에 (직교 투영으로) 없으니(조차장에서 드론 투영측점=162210) 3번 폴백이
|
||||
130m 밖 162210으로 점프. '직교 일치 시에만' 원칙과도 불일치.
|
||||
|
||||
## 수정
|
||||
폴백(882~889) 제거 → `if (!times.length) return;`.
|
||||
TOL(기본 20m) 안에 없으면 그 측점은 (직교로) 존재하지 않으므로 **점프하지 않음**.
|
||||
프레임이 조밀(30fps)해 실제 지나는 측점은 항상 TOL 안에서 잡히므로 폴백 불필요.
|
||||
|
||||
## 효과
|
||||
- 없는 측점 검색 시 엉뚱한 곳으로 안 튐(아무 동작 안 함).
|
||||
- 존재하는 측점(마커/연속측점)은 그대로 검색·순환 이동.
|
||||
|
||||
## 추가: '측점 없음' 안내 (사용자 선택 = 안내+이동안함)
|
||||
'이동 안 함'만으로는 커서가 직전 위치(162210)에 남아 "검색이 거기로 간 것처럼" 보이는 혼동 →
|
||||
검색 실패 시 **안내 메시지** 표시.
|
||||
- `handleJumpToMileage` 가 boolean 반환(이동 true / 못 찾음 false).
|
||||
- [PlaybackControls.tsx](../../client/src/stationbar/components/PlaybackControls/PlaybackControls.tsx):
|
||||
Enter 시 못 찾으면 입력창 옆에 **"측점 없음"**(빨강) 1.8초 표시, 재입력/블러 시 숨김.
|
||||
|
||||
## 추가 2: 검색을 '실제 측점(chain)' 기준으로만 (마커 라벨 기준 제거)
|
||||
증상: 162080 검색 시 여러 162080 위치를 순환하다 '맨 끝에' 162210으로 이동.
|
||||
원인: 검색 1순위가 structureMarks 를 '표시 측점 라벨(round km)'로 매칭 → 라벨은 162080이지만
|
||||
실제 위치가 다른(조차장 좌표근접) 마커가 순환에 끼어듦.
|
||||
|
||||
수정: 검색 1순위(마커 라벨 매칭) 제거. **드론 실제 투영 측점(arr[].chain)이 입력값과 TOL 이내인
|
||||
통과로만** 이동(연속구간=1통과, 순환). 없으면 false → '측점 없음'.
|
||||
- deps 에서 structureMarks/timeAtFrac/timeTrackWidth 제거.
|
||||
- 진단용 HUD 표시 `· 검색v2`(빌드 확인용, 확인 후 제거 예정).
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, `npm run build` 성공 → 해시 `index-BrZ52ILs.js` :55000 서빙 확인.
|
||||
@@ -0,0 +1,25 @@
|
||||
# '측점 없음' 안내가 안 뜨던 문제 수정 (CSS 레이아웃)
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~300k / output ~2k tokens
|
||||
|
||||
## 문제
|
||||
없는 측점 검색 시 "측점 없음" 안내가 화면에 안 떴다(로직은 정상).
|
||||
|
||||
## 원인
|
||||
재생/입력 패널 `.transportGroup` 은 고정 크기 + 자식 요소가 모두 `position: absolute`.
|
||||
안내 `<span>` 이 일반 흐름이라 패널 좌상단에 깔려 버튼/패널 뒤에 가려짐.
|
||||
|
||||
## 수정
|
||||
- [PlaybackControls.module.scss](../../client/src/stationbar/components/PlaybackControls/PlaybackControls.module.scss)
|
||||
에 `.notFound` 클래스 추가: 입력창과 같은 위치·크기로 **절대배치**, `z-index:6`, 어두운 배경 + 빨간 글씨.
|
||||
- [PlaybackControls.tsx](../../client/src/stationbar/components/PlaybackControls/PlaybackControls.tsx)
|
||||
안내 span 을 인라인 스타일 → `className={styles.notFound}` (문구 `⚠ 측점 없음`).
|
||||
- 1.8초 후 자동 숨김 / 재입력 시 숨김(기존 로직 유지).
|
||||
|
||||
## 부수 정리
|
||||
- 진단용 HUD 표시 `· 검색v5` 제거(빌드 확인용이었음).
|
||||
|
||||
## 검증
|
||||
- `tsc --noEmit` 통과, `npm run build` 성공 → :55000 서빙.
|
||||
- 168000(범위 밖) 검색 시 '⚠ 측점 없음' 정상 표시 확인(사용자).
|
||||
@@ -0,0 +1,41 @@
|
||||
# 초기 구동 기본값 변경 — 좌측패널 OFF + 시설등급 1·2종만 체크
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~48k / output ~7k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
> 처음 플레이어가 구동되면 "좌측패널 OFF"이고, 시설등급은 1종·2종만 체크된 상태여야 한다.
|
||||
|
||||
(같은 세션에서 앞서 "로컬 실행 가능 여부", "지도 나침반 가운데버튼 패닝 가능 여부"는 코드 변경 없이 질의응답만 진행.)
|
||||
|
||||
## 변경 파일
|
||||
|
||||
- `client/src/store/settingsStore.ts`
|
||||
|
||||
## 변경 내용
|
||||
|
||||
1. **좌측 노선 패널 기본값**: `showRoutePanel: true → false` (초기 OFF)
|
||||
2. **시설등급 기본 필터**: `DEFAULT_GRADE_FILTER['기타'] true → false`
|
||||
- 결과: 1종☑ 2종☑ 3종☐ 기타☐ → "1종·2종만 체크" 충족
|
||||
3. **persist 마이그레이션 추가**: `version: 1` + `migrate()` 신설
|
||||
- 사유: 설정이 localStorage(`ghivideo.settings`)에 지속되므로, 기본값만 바꾸면 이미 실행해본 브라우저의 옛 저장값(showRoutePanel:true, 기타:true)이 `merge`에서 우선되어 안 바뀜.
|
||||
- 처리: 과거 저장본(version 0)은 최초 1회 `showRoutePanel:false` + `gradeFilter=기본값`으로 강제 재설정. 이후 사용자의 토글 변경은 정상적으로 지속됨.
|
||||
- 신규 브라우저(저장값 없음)는 initializer 기본값이 그대로 적용됨.
|
||||
4. 관련 주석 2곳 갱신.
|
||||
|
||||
## 검증
|
||||
|
||||
- `npm run build -w client` (tsc + vite build) 성공 — 타입 에러 없음, 220 모듈 변환, dist 재생성.
|
||||
- Node 20.20.2(nvm)로 빌드.
|
||||
|
||||
## 반영 방법
|
||||
|
||||
- 프로덕션(55000, PM2): 정적 dist를 서빙하므로 브라우저 **하드 리로드**로 반영(에셋 해시 변경됨). PM2 재시작 불필요.
|
||||
- 개발(5173): dev 서버가 소스를 직접 읽으므로 즉시 반영.
|
||||
- 기존 브라우저는 첫 로드 시 마이그레이션이 1회 동작해 좌측패널 OFF·시설등급 1·2종으로 재설정됨(수동 localStorage 삭제 불필요).
|
||||
|
||||
## 참고 / 판단 사항
|
||||
|
||||
- "처음 구동 시 OFF"를 "매 실행마다 강제 OFF"가 아니라 "초기 기본값 = OFF, 이후 사용자 토글은 기억"으로 해석함. 만약 매 실행마다 무조건 OFF로 시작하길 원하면 `showRoutePanel`을 persist 대상에서 제외(partialize)하는 추가 작업 필요.
|
||||
- `기타` 등급은 이번에 기본 해제로 바뀌어, `isGradeVisible` 규칙상 '기타' 등급 구조물이 기본 화면에서 숨겨짐(요청 문구 "1종·2종만" 준수 결과).
|
||||
@@ -0,0 +1,37 @@
|
||||
# 경영진(사장님) 발표 자료 3장 작성
|
||||
|
||||
**소요 시간**: 약 15분
|
||||
**Context 사용량**: input ~72k / output ~11k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
- 사장님께 GhiVideo 프로그램을 발표·설명할 예정 → **3장 내외 발표 자료** 작성.
|
||||
- 근거 자료: `docs/` 폴더에 취합된 기존 문서들.
|
||||
|
||||
## 산출물
|
||||
|
||||
- `docs/발표_GhiVideo_경영진요약_3장.md` (원본)
|
||||
- `docs/발표_GhiVideo_경영진요약_3장.html` (자체완결)
|
||||
- `docs/발표_GhiVideo_경영진요약_3장.pdf` (A4, 한글 임베드, 3페이지)
|
||||
|
||||
## 구성 (임원 눈높이 · 각 1페이지)
|
||||
|
||||
1. **무엇 / 왜** — 한 줄 요약, 필요성(기존 시간축 탐색의 한계), 핵심 3기능(측점탐색·AR정합·대용량재생), 처리 흐름 도식(SVG)
|
||||
2. **핵심 기술·차별성** — 대표 4기술, 정량 성과 KPI(부각오차 42.7°→11°, DEM 대비 ~0.2m, 4K 60fps, 왕복오차≈0), DEM-free 차별화, 특허 2건
|
||||
3. **현황·기대효과·계획·검토요청** — 동작 제품(55000 시연), 기대효과 표, 향후 로드맵, 의사결정 요청(특허 출원 승인·시범적용)
|
||||
|
||||
## 근거 문서
|
||||
|
||||
- `보고서_GhiVideo_개발내용및핵심기술.md` (핵심 내용·수치·흐름도 SVG 재사용)
|
||||
- `기술명세_GhiVideo_종합기술문서.md`, 각종 특허/쉬운설명 문서
|
||||
|
||||
## 방법 / 검증
|
||||
|
||||
- 기존 파이프라인 `scripts/md2docs.sh`(pandoc + weasyprint, report.css)로 HTML/PDF 생성.
|
||||
- HTML 검증: `break-before: page` 2개(=3페이지) 확인, 흐름도 SVG 포함, 10개 섹션 제목 정상.
|
||||
- PDF 68KB 정상 생성(렌더 이미지 미리보기 도구 poppler 미설치로 화면 캡처 확인은 생략).
|
||||
|
||||
## 참고
|
||||
|
||||
- 파일 상단 KPI·차별화 문구는 사장님 대상이라 기술용어 최소화, 효과·비용·의사결정 중심으로 재구성.
|
||||
- 숫자·특허 현황은 기존 보고서 값 그대로 인용(부각오차·표고 일치도 등).
|
||||
@@ -0,0 +1,34 @@
|
||||
# KMZ → 영상 표출 전 과정 그림 설명서 작성
|
||||
|
||||
**소요 시간**: 약 20분
|
||||
**Context 사용량**: input ~95k / output ~14k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
- "KMZ 파일을 시작으로 동영상에 위치시키기까지의 과정을 논리적으로 그림과 함께 자세히 설명"
|
||||
|
||||
## 산출물
|
||||
|
||||
- `docs/쉬운설명_KMZ에서_영상표출까지_전과정.md` (+ .html / .pdf, md2docs.sh)
|
||||
- SVG 도식 6점: 전체 파이프라인 / KMZ 내부구조·Placemark 해부 / 표고 datum / 좌표 3단 변환 / 핀홀 투영 / cover 정렬·평활
|
||||
|
||||
## 문서 구성 (8단계 + 보너스)
|
||||
|
||||
1. KMZ 압축 해제 — fflate `unzipSync`, `.kml` 우선 (`geoData.ts parseKmz` L295)
|
||||
2. KML 폴더 트리 재귀 + Placemark 수집 (`walk` L376, `parseKmlDescProps` L276)
|
||||
3. 폴더명 기반 POI/구조물 분류 — 교량·터널·구교=구조물, 지장물·출입문=POI, KAKAO_RAIL=POI+역사 (`handlePlacemark` L331)
|
||||
4. 표고 결정 — 드래그보정 > DEM > 최근접 측점 정표고, geoidOffset(대전 25.8m) datum 정합
|
||||
5. 좌표 변환 — 위경도 → EPSG:5186 TM(proj4) → 드론 기준 상대 ENU (`projectPoint` L334)
|
||||
6. 회전 — Rz(−yaw)·Rx(pitch)·Ry(roll) → 카메라 좌표, Zc≤0/FOV/거리 필터 (`toCameraCoords` L301)
|
||||
7. 핀홀 투영 — u=0.5+(Xc/Zc)(f/sW) (`pixelFromCamera` L126)
|
||||
8. 화면 배치 — object-fit:cover 정렬(StationOverlay L948) + 속도적응 평활(smoothStep L49) + 60fps RAF
|
||||
★ 역투영 드래그 보정 (`worldFromPixel` L149)
|
||||
|
||||
## 검증
|
||||
|
||||
- 실제 소스(geoData.ts / geoProjection.ts / StationOverlay.tsx)를 읽고 단계·라인 확인 후 작성 (라인은 2026-07-03 기준).
|
||||
- md2docs.sh 변환 정상(HTML 31KB, PDF 140KB).
|
||||
|
||||
## 참고
|
||||
|
||||
- 초등학생 수준 눈높이(메모리 기본값) + 파일·라인 근거 병기 형식. 기존 쉬운설명 시리즈와 동일한 스타일.
|
||||
@@ -0,0 +1,29 @@
|
||||
# 쉬운설명_프레임기반에서_측점기반_플레이어 — 3절 그림 개선
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~110k / output ~5k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
- 「3. 핵심 비밀 — 매 장면마다 '시간'과 '측점'을 짝지어 둔다」 절의 그림을 더 명료하고 이해하기 쉽게 재작성.
|
||||
|
||||
## 문제 진단 (기존 그림)
|
||||
|
||||
- 마름모꼴 화살표 흐름(위 1박스 → 좌우 2박스 → 아래 1박스)이라 시선 동선이 교차함.
|
||||
- 결과물인 "시간↔측점 표"가 문구로만 존재 — 표(사전)라는 핵심 개념이 시각화되지 않음.
|
||||
- 표를 "왜 만드는지"(재생 중 측점 표시 / 측점 검색 점프)가 그림에 없음.
|
||||
|
||||
## 개선 내용 (docs/쉬운설명_프레임기반에서_측점기반_플레이어.md)
|
||||
|
||||
3단 구성으로 교체 (viewBox 720×330):
|
||||
|
||||
1. **① 사진 1장마다 두 값 계산** — 프레임 카드(번호+GPS) → ⏱시간(파랑)·📐측점(초록) 두 칩으로 분기
|
||||
2. **② '시간↔측점' 표 완성** — 실제 표 모양(헤더+5행, 예시행 191초/192초/193초/⋮/348초, 현재행 하이라이트)으로 시각화. "모든 사진에 반복" 화살표로 ①→② 연결
|
||||
3. **③ 표 하나로 양방향 찾기** — 재생 중("지금 192초, 여긴 어디?"→측점 표시) / 검색("160k130 가줘!"→192초 점프) 두 사용례 배지
|
||||
|
||||
- figcaption도 ①②③ 구조에 맞춰 갱신. 색 체계는 문서 기존 규칙 유지(시간=파랑, 측점=초록, 표=앰버, 화살표=보라).
|
||||
|
||||
## 검증
|
||||
|
||||
- md2docs.sh 재변환 정상 (HTML 24KB, PDF 94KB 재생성).
|
||||
- SVG 좌표 겹침 검토(패널 간격·표 행 경계·하단 배지) 완료.
|
||||
@@ -0,0 +1,28 @@
|
||||
# SVG 내부 빈 줄로 인한 그림 깨짐(빈 화면) 수정
|
||||
|
||||
**소요 시간**: 약 10분
|
||||
**Context 사용량**: input ~125k / output ~6k tokens
|
||||
|
||||
## 증상
|
||||
|
||||
- 「쉬운설명_프레임기반에서_측점기반_플레이어」 3절 새 그림이 HTML/PDF에서 **빈 상자**로 나오고, 그림 속 글자들이 그림 밖 본문에 줄글로 쏟아짐 (사용자 스크린샷 제보).
|
||||
|
||||
## 원인
|
||||
|
||||
- pandoc(gfm)은 **raw HTML 블록을 빈 줄에서 종료**함 → `<svg>` 내부에 빈 줄이 있으면 블록이 쪼개져 이후 `<text>` 등의 내용이 마크다운 본문으로 처리됨.
|
||||
- 직전 작업(3절 그림 개선)에서 가독성용 빈 줄 4개를 SVG 안에 넣은 것이 직접 원인. 추가로 1절 기존 그림(41행)에도 동일 문제 잠재.
|
||||
|
||||
## 수정
|
||||
|
||||
1. `docs/쉬운설명_프레임기반에서_측점기반_플레이어.md` — SVG 내부 빈 줄 5곳 제거 (41, 72, 86, 91, 113행). 주석(`<!-- -->`)은 빈 줄 없이는 무해하므로 유지.
|
||||
2. **전수 검사**(awk, docs/*.md 전체)로 동일 문제 추가 발견: `발표_GhiVideo_좌표투영과_측점기반재생.md` 117행 → 제거.
|
||||
3. 두 문서 md2docs.sh 재변환.
|
||||
|
||||
## 검증
|
||||
|
||||
- md 전수 검사: SVG 내부 빈 줄 0건.
|
||||
- 생성 HTML 파이썬 검사: svg 블록 제거 후 본문에 그림 전용 문구 누출 **없음** ✅ (두 문서 모두), 3절 svg에 text 25개 정상 포함.
|
||||
|
||||
## 재발 방지 메모
|
||||
|
||||
- **SVG를 md에 인라인할 때 `<svg>...</svg>` 사이에 빈 줄 금지.** 구획 구분은 빈 줄 대신 주석 한 줄로.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user