25 KiB
기능: 폴더 선택 기반 영상+데이터 동시 로딩
설계 문서 (① 설계 단계). 이 문서는 구현 지시서이며, 코드 변경은 ②구현 단계 에이전트가 수행한다. 모든 경로는 프로젝트 루트
/home/hanmac/projects/gitea/b23042/DefVideo기준 상대경로로 표기한다.
목표 / 사용자 시나리오
영상과 지리정보(드론 GPS/자세, 측점, POI, 선로 중심선)를 하나의 폴더 선택으로 동시에 로드한다.
현재는 영상은 클라이언트 File API로, 지리정보는 서버가 고정 디렉토리(samplevideo) + 하드코딩 영상명('회덕')으로 따로 로드하므로 두 데이터가 연결되지 않는다.
사용자 시나리오
- 사용자가 영상 플레이어에서 "폴더 선택" 컨트롤을 클릭한다.
- OS 폴더 선택 다이얼로그에서 영상 + 데이터가 들어있는 폴더(예:
samplevideo/)를 고른다. - 브라우저가
webkitdirectory로 폴더 내 모든 파일(하위building/포함)을 노출한다. - 클라이언트가:
- 폴더에서 영상 파일(
.mp4/.MP4/.webm)을 찾아loadLocalFile로 재생한다. <base>.csv(드론 프레임),center.csv(중심선),building/<base>_POI_위경도값.csv,building/<base>_측점_위경도값.csv를 클라이언트에서 직접 파싱해 Zustand 지리정보 스토어에 적재한다.
- 폴더에서 영상 파일(
- 우측 패널(지리정보/측점)과 영상 오버레이(측점/POI/중심선)가 선택한 폴더의 데이터 기준으로 즉시 표시된다.
- 영상명에 의존하지 않는다:
'회덕'하드코딩 제거. 동일한 폴더 구조면 어떤 영상이든 동작한다.
폴백
- 폴더가 아직 선택되지 않았으면 지리정보 스토어는 비어 있다. 각 소비 컴포넌트는 빈 상태(측점 0개, 검색 결과 없음, 오버레이 미표시)를 표시한다.
- (선택) 스토어가 비었을 때 기존
/api/geo/*서버 API로 폴백할 수 있으나, 기본 동작은 클라이언트 스토어 우선. 이 문서에서는 서버 폴백을 "옵션"으로 두고, 기본 구현은 스토어 단일 소스로 한다.
제약
- 브라우저 폴더 접근:
<input type="file" webkitdirectory>사용. Chrome/Firefox/Edge(PC) 지원 — 프로젝트 대상 브라우저와 일치(CLAUDE.md). iOS Safari 비대상.- 선택된 각
File은file.webkitRelativePath(예:samplevideo/building/...csv)로 폴더 내 상대경로를 노출한다. - 첫 세그먼트는 사용자가 고른 루트 폴더명이므로, 매칭 시 basename + 마지막 디렉토리 세그먼트로 판단한다.
- 선택된 각
- 클라이언트 전용: 서버 의존 없음. 파일 업로드 없이 File API로 직접 처리.
- 인코딩: POI/측점 CSV와
center.csv는 EUC-KR(서버는iconv-lite로 디코딩). 클라이언트에는iconv-lite가 없으므로 브라우저TextDecoder('euc-kr')(Encoding API legacy label, Chrome/FF/Edge 지원)로 디코딩한다. 드론<base>.csv는 UTF-8 + BOM. - 메모리: 영상은
URL.createObjectURL()로 직접 재생, 사용 후revokeObjectURL(기존useVideoPlayer.loadLocalFile가 이미 처리). - 좌표 투영: 클라이언트는 이미
proj4(EPSG:5186) 기반 투영 유틸을 보유. 서버geoMatch.ts는 단순 ENU(cos-lat 근사) 방식이라 투영식이 다름 — 데이터 로딩/파싱/검색 로직만 포팅하고 투영은 기존 클라이언트 유틸(geoProjection.ts)을 재사용한다(아래 상세).
파일 명명 규칙 & CSV 스키마
<base> = 영상 파일명에서 확장자를 제거한 이름. 예: 하행)회덕-대전조차장.MP4 → base 하행)회덕-대전조차장.
<folder>/
<base>.MP4 (또는 .mp4/.webm) → 영상 (loadLocalFile / createObjectURL)
<base>.csv → 드론 프레임 (UTF-8 BOM)
<base>.srt → 프레임 메타(현재 geo 시스템 미파싱 — 주의)
center.csv → 중심선 (224행, EUC-KR)
building/<base>_POI_위경도값.csv → POI 건물 (EUC-KR)
building/<base>_측점_위경도값.csv → 측점 (EUC-KR)
building/<base>_POI_XY값.csv → 미사용
building/<base>_측점_XY값.csv → 미사용
1) 드론 프레임 <base>.csv (UTF-8, BOM 있음)
헤더(BOM 제거 후): frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len
frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len
0,36.402256,127.421515,84.244,114.7,-29.8,0.0,24.00
1,36.402256,127.421515,84.244,114.7,-29.8,0.0,24.00
- 컬럼은 헤더 이름으로 인덱스 조회(
header.indexOf('frame_cnt')등). 서버geoMatch.ts:290-302와 동일. - 매핑:
frame=frame_cnt(int),lat=latitude,lon=longitude,altitude,yaw,pitch,roll,focalLen=focal_len. !isNaN(lat)행만 채택.- 구분자
,. 따옴표 처리는parseCsvLine동일 규칙.
2) 중심선 center.csv (EUC-KR — 헤더 한글이 깨지므로 위치 인덱스 직접 사용)
헤더(깨진 상태): id,lat,lon,표고(H),지오이드높이(N),타원체고(h),67.39,...,x,y
id,lat,lon,<EUC-KR깨짐>,<깨짐>,<깨짐(h)>,67.39,41.57...,,x,y
0,36.4087069,127.4256135,44.5959549,25.449,70.0449549,2.6549549,...,238176.0008,423480.0717
- 서버
geoMatch.ts:251-257규칙: 인덱스 1=lat, 2=lon, 5=타원체고(h)=z. 헤더 이름 의존 금지(EUC-KR 깨짐 회피). !isNaN(lat&&lon&&z)행만 채택. 헤더 1행 skip.
3) POI building/<base>_POI_위경도값.csv (EUC-KR)
헤더: title,category_clean,address_road,address_jibun,lat,lon,z
title,category_clean,address_road,address_jibun,lat,lon,z
한국철도공사대전철도역사,역사,대전 ...,대전 ...,36.371101,127.421776,14.93833585
대전역,역사,...,...,36.380398,127.422422,17.43774457
- 매핑(헤더 이름 조회,
geoMatch.ts:323-335):title,category = category_clean || '건물',lat,lon,z = z || 0,type='poi'. - 파일 선택 우선순위(
geoMatch.ts:317-320): 파일명에타원체고포함 버전이 있으면 그것 우선, 없으면타원체고미포함 버전. (현재 샘플엔타원체고파일 없음 → 기본_POI_위경도값.csv사용.) category_clean값도 EUC-KR(터널/교량/역사/지장물).TextDecoder('euc-kr')로 정상 복원돼야 RoutePanel 카테고리 필터/이모지가 동작한다.
4) 측점 building/<base>_측점_위경도값.csv (EUC-KR)
헤더: title,category_clean,address_road,address_jibun,lat,lon,z (POI와 동일)
title,category_clean,address_road,address_jibun,lat,lon,z
157K900,,,,36.40296455,127.4218765,0.023576703
158K000,,,,36.40222619,127.4212379,-0.148290668
- 매핑(
geoMatch.ts:345-355):title,category='측점'(강제),lat,lon,z = z || 0,type='station'. title은157K900형태(km 정렬에 사용: 정규식/(\d+)[Kk](\d+)/).
5) SRT <base>.srt (UTF-8) — 참고용, 현재 geo 미사용
1
00:00:00,000 --> 00:00:00,033
FrameCnt: 0 2026-03-05 17:18:28.405
[... focal_len: 24.00] [latitude: 36.402256] [longitude: 127.421515] [rel_alt: 16.853 abs_alt: 84.244] [gb_yaw: 114.7 gb_pitch: -29.8 gb_roll: 0.0] ...
- 서버
geoMatch.ts:208-222 loadTerrainOffset만 SRT를 읽어terrain offset = abs_alt - rel_alt를 계산하나, 현재 코드 경로에서 이 offset은 어디에도 적용되지 않는다(_terrainOffset은 set만 되고 사용처 없음). → 이번 포팅에서 SRT는 파싱하지 않아도 무방(현 동작 동일). 문서상 명시.
현재 구조 (As-Is)
영상 선택
client/src/components/player/VideoPlayer.tsx:194-205— "파일 선택" 라벨 +<input type="file" accept="video/*">,onChange에서loadLocalFile(file)호출.- 드래그앤드롭:
VideoPlayer.tsx:96-100. client/src/hooks/useVideoPlayer.ts:63-81 loadLocalFile—URL.createObjectURL(file)→player.src(...)→store.setSource({kind:'local', file, objectUrl})→emptied시revokeObjectURL.- 소스 타입:
client/src/types/player.ts:1-3 VideoSource.
지리정보 (서버 단일 소스)
- 데이터 디렉토리:
server/src/routes/geo.ts:8-11—GEO_DATA_DIR = process.env.GEO_DATA_DIR || <root>/samplevideo,setGeoDataDir()호출. - 서버 서비스
server/src/services/geoMatch.ts(싱글턴 캐시_frames/_pois/_centerline):- CSV 파서
parseCsvLine(162-173),readCsvUtf8(175-192, EUC-KR/UTF-8 BOM 자동 판별,iconv-lite). - 투영:
geoToEnu(78-87, cos-lat 근사 ENU),projectEnu(90-124),project3D(127-156). - 로더:
loadFrames(275-305),loadPois(307-361),loadCenterline(236-261).- ⚠
loadFrames:285—f.includes('회덕')하드코딩. (제거 대상) loadCenterline:239—CENTER_CSV_PATH또는pythonsource/input/center.csv경로(샘플 폴더의 center.csv와 별개일 수 있음).
- ⚠
- 원점:
getWorldOrigin(378-387) — 첫 측점(stationOrder정렬) 사용, 없으면 frame[0]. - 검색 API:
findFramesForPoi(399-468),findPoisForFrame(473-512),getAllPois(515-517),getDroneFrames(520-522),getCenterlinePoints(263-265).
- CSV 파서
- HTTP 라우트
server/src/routes/geo.ts:GET /api/geo/pois(14-21) →getAllPois()→GeoPoint[]GET /api/geo/search?q=&margin=&maxDist=&yawOffset=(27-45) →{poi, frames: FrameMatch[]}GET /api/geo/frame/:frameNum?margin=&yawOffset=(51-64) →{droneFrame, pois: PoiInFrame[]}GET /api/geo/frames?step=(70-79) → 샘플링된DroneFrame[]GET /api/geo/centerline(85-92) →CenterlinePoint[]
클라이언트 투영 (이미 존재 — 재사용)
client/src/utils/geoProjection.ts:proj4EPSG:5186 정의(70-72),latLonToTM(76-80),geoToEnu(86-92, TM 기반).CameraParams/DEFAULT_CAMERA_PARAMS(35-62),paramsFromFrame(65-67).toCameraCoords(175-184),pixelFromCamera(115-126),projectPoint(201-294).- 오버레이(StationOverlay/RoutePanel)는 이 유틸로 자체 투영한다. 서버
project3D와 다른 알고리즘. → 검색용findFramesForPoi/findPoisForFrame만 서버에 의존.
클라이언트 소비 컴포넌트 (각 fetch 정리)
| 컴포넌트 | fetch | 기대 응답 형태 | 용도 |
|---|---|---|---|
GeoSearch.tsx:61 |
GET /api/geo/pois |
GeoPoint[] |
자동완성 목록 |
GeoSearch.tsx:82 |
GET /api/geo/search?q=&margin=1.0&maxDist=1500 |
{poi:GeoPoint, frames:FrameMatch[]} |
건물→프레임 검색 |
GeoSearch.tsx:98 |
GET /api/geo/frame/{currentFrame}?margin=1.0 |
{pois:PoiInFrame[]} |
프레임→건물 역조회 |
StationVerify.tsx:57 |
GET /api/geo/pois |
GeoPoint[] (station만 필터) |
측점 목록 |
StationVerify.tsx:69 |
GET /api/geo/frames?step=1 |
{frame,lat,lon}[] |
GPS 최근접 프레임 |
StationVerify.tsx:102 |
GET /api/geo/search?q=&margin=1.2&maxDist=2000 |
{poi,frames:FrameMatch[]} |
검증 정보 |
StationOverlay.tsx:186 |
GET /api/geo/pois |
GeoPoint[] (station/poi 분리) |
라벨 사전계산 |
StationOverlay.tsx:197 |
GET /api/geo/centerline |
CenterlinePoint[] |
중심선 |
StationOverlay.tsx:206 |
GET /api/geo/frames?step=1 |
DroneFrameBasic[] |
드론 프레임 |
RoutePanel.tsx:75 |
GET /api/geo/pois |
GeoPoint[] |
미니맵 측점/POI |
RoutePanel.tsx:87 |
GET /api/geo/frames?step=1 |
DroneFrameBasic[] |
현재 km 계산 |
상태/배선
client/src/App.tsx— 우측 탭rightTab: 'annotation'|'geo'|'station'(19), GeoSearch(151-157)/StationVerify(158-163) 렌더.currentFrame = round(currentTime*fps)(35).client/src/store/playerStore.ts— 영상 재생 상태(source/currentTime/fps 등). 지리정보 스토어 없음.playerRef.loadServerStream은 좌측 VideoList에서,loadLocalFile은 VideoPlayer 내부 input/드롭에서 호출.
변경 설계 (To-Be)
개요
- 폴더 선택 UI → 폴더 내 파일 목록(
File[]+webkitRelativePath)을 단일 파서 진입점에 넘긴다. - 새 클라이언트 지리정보 스토어(
geoStore)가 파싱 결과(frames/pois/stations/centerline/origin)를 보관. - CSV 파싱 + 서버
geoMatch검색 로직을 클라이언트 유틸로 포팅(투영은 기존geoProjection.ts재사용). - 4개 소비 컴포넌트의
/api/geo/*fetch →geoStore셀렉터 + 포팅 함수 호출로 교체. - 영상 파일은 폴더에서 찾아 기존
loadLocalFile로 재생.
New: 폴더 선택 UI
파일: client/src/components/player/VideoPlayer.tsx 수정.
- 기존 "파일 선택"(194-205) 옆에 "폴더 선택" 라벨+input 추가:
<label className="cursor-pointer bg-emerald-600 hover:bg-emerald-700 ... rounded"> 폴더 선택 <input type="file" className="hidden" // @ts-expect-error - 비표준 속성 webkitdirectory="" directory="" multiple onChange={(e) => { const files = e.target.files; if (files?.length) onSelectFolder(Array.from(files)); }} /> </label>webkitdirectory타입 보강:client/src/types/에 전역 선언 추가하거나// @ts-expect-error. (권장:client/src/vite-env.d.ts또는 새client/src/types/dom.d.ts에HTMLInputElement확장 선언.)
onSelectFolder(files: File[])핸들러(VideoPlayer 내부 또는 App에서 전달):loadFolderGeoData(files)호출 → 파싱 +geoStore채움(영상 File도 함께 반환/식별).- 영상 File을 찾으면
loadLocalFile(videoFile).
- 기존 "파일 선택"(단일 영상)은 유지(지리정보 없이 영상만).
New: 클라이언트 지리정보 스토어 (Zustand)
신규 파일: client/src/store/geoStore.ts
import { create } from 'zustand';
// 타입은 client/src/types/geo.ts(신규)로 분리 권장
export interface DroneFrame { frame:number; lat:number; lon:number; altitude:number; yaw:number; pitch:number; roll:number; focalLen:number; }
export interface GeoPoint { title:string; category:string; lat:number; lon:number; z:number; type:'poi'|'station'; }
export interface CenterlinePoint { lat:number; lon:number; z:number; }
interface GeoStore {
loaded: boolean;
frames: DroneFrame[];
pois: GeoPoint[]; // type==='poi'
stations: GeoPoint[]; // type==='station' (stationOrder 정렬)
centerline: CenterlinePoint[];
origin: { lat:number; lon:number; alt:number } | null; // 첫 측점 = ENU 원점
baseName: string | null;
setGeoData: (d: Partial<GeoStore>) => void;
reset: () => void;
}
export const useGeoStore = create<GeoStore>((set) => ({ /* 초기값 + setGeoData/reset */ }));
pois/stations는getAllPois()결과를type으로 분리해 저장(소비처 편의). 또는 통합pois하나만 두고 셀렉터로 분리해도 됨 — 분리 저장 권장(현 컴포넌트들이 매번 filter 함).origin= 첫 측점(stationOrder최소). 서버getWorldOrigin동치.
New: 클라이언트 CSV 파싱 + geoMatch 포팅
신규 파일: client/src/utils/geoData.ts (파싱 + 로딩 + 검색 포팅)
인코딩/파싱 헬퍼 (서버 readCsvUtf8/parseCsvLine 포팅)
parseCsvLine(line)— 서버geoMatch.ts:162-173그대로 포팅(순수 함수, 의존성 없음).decodeBytes(buf: ArrayBuffer, encoding:'utf-8'|'euc-kr')—new TextDecoder(encoding).decode(buf). UTF-8 BOM 제거(replace(/^/,'')). EUC-KR은TextDecoder('euc-kr').- 드론 csv: UTF-8(BOM). POI/측점/center: EUC-KR.
- 서버처럼 자동판별(
iconv)이 아니라 파일 종류별로 인코딩 지정(폴더 구조 알고 있으므로 단순). center/POI/측점은 EUC-KR 고정.
readCsv(file: File, encoding)→Promise<string[][]>—await file.arrayBuffer()→ decode →split(/\r?\n/).filter(Boolean).map(parseCsvLine).
로더 (서버 loader 포팅, but 파일은 File[]에서 매칭)
findVideoFile(files)— 확장자.mp4/.webm(대소문자 무시),building/미포함. → base 추출.parseDroneFrames(file): Promise<DroneFrame[]>— 서버loadFrames:289-302로직(헤더 이름 인덱스).회덕필터 제거, base 일치 또는building/미포함 루트 csv로 식별.parsePois(poiFile, stationFile): Promise<GeoPoint[]>— 서버loadPois:321-357포팅. POI=category_clean||'건물',type:'poi'; 측점=category:'측점',type:'station'. 파일 선택은building/내webkitRelativePath에서POI+위경도,측점+위경도매칭(타원체고우선규칙 유지).parseCenterline(file): Promise<CenterlinePoint[]>— 서버loadCenterline:253-257포팅(인덱스 1/2/5, EUC-KR).loadFolderGeoData(files: File[])— 위 파서들을 호출해geoStore.setGeoData({...})채우고, 영상 File을 반환.
검색 함수 포팅 (서버 geoMatch.ts → 클라이언트)
검색은 서버의 단순 ENU project3D(geoMatch.ts) 알고리즘을 그대로 포팅해 서버 응답과 동일 결과를 보장한다(오버레이용 geoProjection.ts와는 별개 — 검색 UI 호환 우선).
| 포팅 함수 | 서버 소스 라인 | 비고 |
|---|---|---|
toRad |
geoMatch.ts:63 |
|
geoToEnu (cos-lat ENU) |
geoMatch.ts:78-87 |
검색 전용. 오버레이는 기존 proj4 ENU 사용 |
projectEnu |
geoMatch.ts:90-124 |
|
project3D |
geoMatch.ts:127-156 |
|
stationOrder |
geoMatch.ts:371-375 |
(이미 컴포넌트들에 중복 존재 — 공용화) |
getWorldOrigin |
geoMatch.ts:378-387 |
스토어 origin으로 대체 가능 |
findFramesForPoi(query, margin, maxDist, yawOffset) |
geoMatch.ts:399-468 |
스토어 frames/pois 입력으로 변경 |
findPoisForFrame(frameNum, margin, yawOffset) |
geoMatch.ts:473-512 |
동일 |
- 입력을 싱글턴 캐시(
loadFrames()) 대신geoStore데이터를 인자/직접 read로 변경. 반환 타입은 서버와 동일(FrameMatch/PoiInFrame) → 컴포넌트 인터페이스 무변경. DEFAULT_FPS=30,GAP=30등 상수 동일 포팅.
Changes: 소비 컴포넌트 (fetch → 스토어)
각 컴포넌트는 내부 interface GeoPoint/FrameMatch 중복 선언을 types/geo.ts로 통합(선택, 최소변경 시 유지 가능). 핵심은 fetch 제거:
GeoSearch.tsx:61fetch('/api/geo/pois')→useGeoStore의[...stations, ...pois].:82/api/geo/search→ 포팅findFramesForPoi(q, 1.0, 1500, 0)직접 호출(동기).loading/error는 데이터 없을 때만 의미.:98/api/geo/frame/{n}→ 포팅findPoisForFrame(currentFrame, 1.0).
StationVerify.tsx:57pois → 스토어stations.:69frames({frame,lat,lon}) → 스토어frames(필드 일부만 사용).:102search → 포팅findFramesForPoi(title, 1.2, 2000).
StationOverlay.tsx:186pois → 스토어stations/pois(이미 station/poi 분리 사용).updateWorldOrigin은 스토어origin으로 대체.:197centerline → 스토어centerline.:206frames → 스토어frames.- 투영은 기존
geoProjection.ts그대로(변경 없음).
RoutePanel.tsx:75pois → 스토어.:87frames → 스토어.
- 폴백/가드: 스토어
loaded===false이면 빈 배열로 동작(현재 fetch 실패 시와 동일한 빈 상태).useEffect의[visible]의존성은 스토어 구독으로 대체(데이터 도착 시 자동 재렌더).- (옵션) 스토어가 비어 있을 때 기존
/api/geo/*로 폴백하려면 각 컴포넌트에if (store.loaded) useStore else fetch분기. 기본 구현은 스토어 단일 소스 권장(서버 의존 제거가 목표).
- (옵션) 스토어가 비어 있을 때 기존
영상 재생 배선
loadFolderGeoData가 반환한 영상File→playerRef.loadLocalFile(file)(기존 경로 그대로).- 영상이 폴더에 없으면 지리정보만 로드(오버레이는 동작하나 재생 화면 없음) — 에러 토스트/콘솔 경고.
App.tsx: 폴더 선택 핸들러를 VideoPlayer로 내려보내거나 VideoPlayer 내부에서loadLocalFile(이미 보유) +loadFolderGeoData직접 호출. VideoPlayer 내부 처리 권장(이미loadLocalFile접근 가능).
구현 태스크 목록
client/src/types/geo.ts신규 —DroneFrame/GeoPoint/CenterlinePoint/FrameMatch/PoiInFrame공용 타입.client/src/store/geoStore.ts신규 — Zustand 스토어(frames/pois/stations/centerline/origin/baseName/loaded + setGeoData/reset).client/src/utils/geoData.ts신규:parseCsvLine포팅(servergeoMatch.ts:162-173).decodeBytes/readCsv(TextDecoder utf-8 + euc-kr, BOM 제거).findVideoFile(files)(mp4/MP4/webm, building 제외, base 추출).parseDroneFrames(헤더 인덱스, 회덕 하드코딩 없음).parsePois(POI: category_clean||건물 / 측점: '측점', 타원체고 우선규칙).parseCenterline(인덱스 1/2/5, EUC-KR).loadFolderGeoData(files)→ setGeoData + return videoFile.
client/src/utils/geoSearch.ts신규(또는 geoData.ts 내) —toRad/geoToEnu/projectEnu/project3D/stationOrder/findFramesForPoi/findPoisForFrame포팅(servergeoMatch.ts). 입력은 스토어 데이터.client/src/types/dom.d.ts(또는 vite-env.d.ts) —HTMLInputElementwebkitdirectory/directory보강.client/src/components/player/VideoPlayer.tsx— "폴더 선택" UI 추가 +onSelectFolder→loadFolderGeoData+loadLocalFile.client/src/components/geo/GeoSearch.tsx— fetch 3곳 제거 → 스토어/포팅 함수.client/src/components/geo/StationVerify.tsx— fetch 3곳 제거 → 스토어/포팅 함수.client/src/components/overlay/StationOverlay.tsx— fetch 3곳 제거 → 스토어 구독, origin 스토어화.client/src/components/overlay/RoutePanel.tsx— fetch 2곳 제거 → 스토어.- (정리) 컴포넌트 내 중복
stationOrder/GeoPoint선언을 공용 타입/유틸로 통합. npm run build -w client통과 확인(tsc + vite).- 수동 검증:
samplevideo/폴더 선택 → 영상 재생 + 측점/POI/중심선 오버레이 + 측점 패널 목록 표시. 회덕등 영상명 하드코딩이 클라이언트 경로에 없음을 확인(grep -r 회덕 client/src→ 0건; RoutePanel 주석의 '대전조차장/회덕'은 단순 주석이므로 무관).
완료 기준
npm run build -w client(=tsc && vite build)가 타입 에러 없이 통과.- "폴더 선택"으로
samplevideo/(또는 동일 구조 폴더) 선택 시:- 폴더 내 영상이 재생된다(
createObjectURL). - 측점/POI/선로 중심선 오버레이가 표시된다(
StationOverlay). - 우측 "측점" 탭에 측점 목록이 채워지고, 클릭 시 해당 프레임으로 이동한다.
- "지리정보" 탭 검색/역조회가 클라이언트 데이터로 동작한다.
- RoutePanel 미니맵이 시점/종점/현재 km를 표시한다.
- 폴더 내 영상이 재생된다(
- 동작이 영상명 비의존적:
'회덕'등 하드코딩 없음. 다른 base명 폴더도 동일 구조면 동작. - EUC-KR 한글(측점명/POI 카테고리)이 깨지지 않고 표시된다(
TextDecoder('euc-kr')).
토큰 사용 기록
추정치(k = 1,000 tokens). 각 에이전트 자체 보고 입력/출력 추정 + 측정된 subagent 총 토큰(측정 합계) 기준.
| 단계 | 에이전트 | 입력(추정) | 출력(추정) | 측정 합계 |
|---|---|---|---|---|
| ①설계 | DESIGN/DOCUMENTATION | ~46k | ~9k | 98.7k |
| ②a 구현·기반 | DEV-FOUNDATION (types/store/utils + 폴더선택 UI) | ~60k | ~9k | 99.3k |
| ②b 구현·소비 | DEV-CONSUMER: GeoSearch | ~30k | ~2k | 52.0k |
| ②b 구현·소비 | DEV-CONSUMER: StationVerify | ~24k | ~2k | 48.1k |
| ②b 구현·소비 | DEV-CONSUMER: StationOverlay | ~32k | ~3k | 66.8k |
| ②b 구현·소비 | DEV-CONSUMER: RoutePanel | ~30k | ~3k | 55.8k |
| ③검증 | REVIEW/검수 | ~62k | ~3k | 105.1k |
| — | (기반 1차 시도, 529 과부하로 무산) | 0 | 0 | 0 |
| 합계 | 7개 에이전트 | ~284k | ~31k | ≈526k |
②b 4개 소비 에이전트는 병렬 실행(서로 다른 파일)으로 벽시계 시간 단축. 판정: ship (빌드 통과, blocker 0, 발견 이슈 전부 nit).
후속 수정 (2026-06-17)
🔴 누락 컴포넌트: StationBar (측점 바) — 사용자 보고 "측점 패널 안 보임"
- 원인: 초기 탐색이 geo 소비 컴포넌트로 GeoSearch/StationVerify/StationOverlay/RoutePanel 4개만 식별. 영상 하단의 측점 바 client/src/stationbar/StationBar.tsx는 누락되어, 여전히
/api/geo/pois·/api/geo/frames를 직접 fetch. → 폴더 선택으로 채운 클라이언트 데이터를 못 읽고, 서버 미기동 시 측점이 표시되지 않음. - 수정: StationBar를 다른 컴포넌트와 동일하게 geoStore로 연결.
pois=[...stations, ...pois](store 파생),frames=storeFrames로 교체. 두fetchuseEffect 제거.- precompute/세그먼트 effect 의존성
framesVersion→storeFrames. 데이터 비었을 때setReady(false)로 idle 복귀. npm run build -w client통과 확인. 클라이언트 전체에 live/api/geofetch 0건.
- 교훈: 다음 탐색 시
grep -rn "fetch(.*\/api\/geo"로 소비처 전수 조사를 먼저 수행해 누락 방지.