Files
DefVideo/agent-docs/feature-folder-load.md
b23042 819065a8f5 UI 수정
기획안 반영 및 보완
2026-06-19 14:40:47 +09:00

25 KiB
Raw Blame History

기능: 폴더 선택 기반 영상+데이터 동시 로딩

설계 문서 (① 설계 단계). 이 문서는 구현 지시서이며, 코드 변경은 ②구현 단계 에이전트가 수행한다. 모든 경로는 프로젝트 루트 /home/hanmac/projects/gitea/b23042/DefVideo 기준 상대경로로 표기한다.


목표 / 사용자 시나리오

영상과 지리정보(드론 GPS/자세, 측점, POI, 선로 중심선)를 하나의 폴더 선택으로 동시에 로드한다. 현재는 영상은 클라이언트 File API로, 지리정보는 서버가 고정 디렉토리(samplevideo) + 하드코딩 영상명('회덕')으로 따로 로드하므로 두 데이터가 연결되지 않는다.

사용자 시나리오

  1. 사용자가 영상 플레이어에서 "폴더 선택" 컨트롤을 클릭한다.
  2. OS 폴더 선택 다이얼로그에서 영상 + 데이터가 들어있는 폴더(예: samplevideo/)를 고른다.
  3. 브라우저가 webkitdirectory로 폴더 내 모든 파일(하위 building/ 포함)을 노출한다.
  4. 클라이언트가:
    • 폴더에서 영상 파일(.mp4/.MP4/.webm)을 찾아 loadLocalFile로 재생한다.
    • <base>.csv(드론 프레임), center.csv(중심선), building/<base>_POI_위경도값.csv, building/<base>_측점_위경도값.csv클라이언트에서 직접 파싱해 Zustand 지리정보 스토어에 적재한다.
  5. 우측 패널(지리정보/측점)과 영상 오버레이(측점/POI/중심선)가 선택한 폴더의 데이터 기준으로 즉시 표시된다.
  6. 영상명에 의존하지 않는다: '회덕' 하드코딩 제거. 동일한 폴더 구조면 어떤 영상이든 동작한다.

폴백

  • 폴더가 아직 선택되지 않았으면 지리정보 스토어는 비어 있다. 각 소비 컴포넌트는 빈 상태(측점 0개, 검색 결과 없음, 오버레이 미표시)를 표시한다.
  • (선택) 스토어가 비었을 때 기존 /api/geo/* 서버 API로 폴백할 수 있으나, 기본 동작은 클라이언트 스토어 우선. 이 문서에서는 서버 폴백을 "옵션"으로 두고, 기본 구현은 스토어 단일 소스로 한다.

제약

  • 브라우저 폴더 접근: <input type="file" webkitdirectory> 사용. Chrome/Firefox/Edge(PC) 지원 — 프로젝트 대상 브라우저와 일치(CLAUDE.md). iOS Safari 비대상.
    • 선택된 각 Filefile.webkitRelativePath(예: samplevideo/building/...csv)로 폴더 내 상대경로를 노출한다.
    • 첫 세그먼트는 사용자가 고른 루트 폴더명이므로, 매칭 시 basename + 마지막 디렉토리 세그먼트로 판단한다.
  • 클라이언트 전용: 서버 의존 없음. 파일 업로드 없이 File API로 직접 처리.
  • 인코딩: POI/측점 CSV와 center.csvEUC-KR(서버는 iconv-lite로 디코딩). 클라이언트에는 iconv-lite가 없으므로 브라우저 TextDecoder('euc-kr')(Encoding API legacy label, Chrome/FF/Edge 지원)로 디코딩한다. 드론 <base>.csvUTF-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'.
  • title157K900 형태(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 loadLocalFileURL.createObjectURL(file)player.src(...)store.setSource({kind:'local', file, objectUrl})emptiedrevokeObjectURL.
  • 소스 타입: client/src/types/player.ts:1-3 VideoSource.

지리정보 (서버 단일 소스)

  • 데이터 디렉토리: server/src/routes/geo.ts:8-11GEO_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:285f.includes('회덕') 하드코딩. (제거 대상)
      • loadCenterline:239CENTER_CSV_PATH 또는 pythonsource/input/center.csv 경로(샘플 폴더의 center.csv와 별개일 수 있음).
    • 원점: getWorldOrigin(378-387) — 첫 측점(stationOrder 정렬) 사용, 없으면 frame[0].
    • 검색 API: findFramesForPoi(399-468), findPoisForFrame(473-512), getAllPois(515-517), getDroneFrames(520-522), getCenterlinePoints(263-265).
  • HTTP 라우트 server/src/routes/geo.ts:
    • GET /api/geo/pois(14-21) → getAllPois()GeoPoint[]
    • GET /api/geo/search?q=&margin=&maxDist=&yawOffset=(27-45) → {poi, frames: FrameMatch[]}
    • GET /api/geo/frame/:frameNum?margin=&yawOffset=(51-64) → {droneFrame, pois: PoiInFrame[]}
    • GET /api/geo/frames?step=(70-79) → 샘플링된 DroneFrame[]
    • GET /api/geo/centerline(85-92) → CenterlinePoint[]

클라이언트 투영 (이미 존재 — 재사용)

  • client/src/utils/geoProjection.ts:
    • proj4 EPSG:5186 정의(70-72), latLonToTM(76-80), geoToEnu(86-92, TM 기반).
    • CameraParams / DEFAULT_CAMERA_PARAMS(35-62), paramsFromFrame(65-67).
    • toCameraCoords(175-184), pixelFromCamera(115-126), projectPoint(201-294).
    • 오버레이(StationOverlay/RoutePanel)는 이 유틸로 자체 투영한다. 서버 project3D와 다른 알고리즘. → 검색용 findFramesForPoi/findPoisForFrame만 서버에 의존.

클라이언트 소비 컴포넌트 (각 fetch 정리)

컴포넌트 fetch 기대 응답 형태 용도
GeoSearch.tsx:61 GET /api/geo/pois GeoPoint[] 자동완성 목록
GeoSearch.tsx:82 GET /api/geo/search?q=&margin=1.0&maxDist=1500 {poi:GeoPoint, frames:FrameMatch[]} 건물→프레임 검색
GeoSearch.tsx:98 GET /api/geo/frame/{currentFrame}?margin=1.0 {pois:PoiInFrame[]} 프레임→건물 역조회
StationVerify.tsx:57 GET /api/geo/pois GeoPoint[] (station만 필터) 측점 목록
StationVerify.tsx:69 GET /api/geo/frames?step=1 {frame,lat,lon}[] GPS 최근접 프레임
StationVerify.tsx:102 GET /api/geo/search?q=&margin=1.2&maxDist=2000 {poi,frames:FrameMatch[]} 검증 정보
StationOverlay.tsx:186 GET /api/geo/pois GeoPoint[] (station/poi 분리) 라벨 사전계산
StationOverlay.tsx:197 GET /api/geo/centerline CenterlinePoint[] 중심선
StationOverlay.tsx:206 GET /api/geo/frames?step=1 DroneFrameBasic[] 드론 프레임
RoutePanel.tsx:75 GET /api/geo/pois GeoPoint[] 미니맵 측점/POI
RoutePanel.tsx:87 GET /api/geo/frames?step=1 DroneFrameBasic[] 현재 km 계산

상태/배선

  • client/src/App.tsx — 우측 탭 rightTab: 'annotation'|'geo'|'station'(19), GeoSearch(151-157)/StationVerify(158-163) 렌더. currentFrame = round(currentTime*fps)(35).
  • client/src/store/playerStore.ts — 영상 재생 상태(source/currentTime/fps 등). 지리정보 스토어 없음.
  • playerRef.loadServerStream은 좌측 VideoList에서, loadLocalFile은 VideoPlayer 내부 input/드롭에서 호출.

변경 설계 (To-Be)

개요

  1. 폴더 선택 UI → 폴더 내 파일 목록(File[] + webkitRelativePath)을 단일 파서 진입점에 넘긴다.
  2. 클라이언트 지리정보 스토어(geoStore)가 파싱 결과(frames/pois/stations/centerline/origin)를 보관.
  3. CSV 파싱 + 서버 geoMatch 검색 로직을 클라이언트 유틸로 포팅(투영은 기존 geoProjection.ts 재사용).
  4. 4개 소비 컴포넌트의 /api/geo/* fetch → geoStore 셀렉터 + 포팅 함수 호출로 교체.
  5. 영상 파일은 폴더에서 찾아 기존 loadLocalFile로 재생.

New: 폴더 선택 UI

파일: client/src/components/player/VideoPlayer.tsx 수정.

  • 기존 "파일 선택"(194-205) 옆에 "폴더 선택" 라벨+input 추가:
    <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.tsHTMLInputElement 확장 선언.)
  • onSelectFolder(files: File[]) 핸들러(VideoPlayer 내부 또는 App에서 전달):
    1. loadFolderGeoData(files) 호출 → 파싱 + geoStore 채움(영상 File도 함께 반환/식별).
    2. 영상 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/stationsgetAllPois() 결과를 type으로 분리해 저장(소비처 편의). 또는 통합 pois 하나만 두고 셀렉터로 분리해도 됨 — 분리 저장 권장(현 컴포넌트들이 매번 filter 함).
  • origin = 첫 측점(stationOrder 최소). 서버 getWorldOrigin 동치.

New: 클라이언트 CSV 파싱 + geoMatch 포팅

신규 파일: client/src/utils/geoData.ts (파싱 + 로딩 + 검색 포팅)

인코딩/파싱 헬퍼 (서버 readCsvUtf8/parseCsvLine 포팅)

  • parseCsvLine(line) — 서버 geoMatch.ts:162-173 그대로 포팅(순수 함수, 의존성 없음).
  • decodeBytes(buf: ArrayBuffer, encoding:'utf-8'|'euc-kr')new TextDecoder(encoding).decode(buf). UTF-8 BOM 제거(replace(/^/,'')). EUC-KR은 TextDecoder('euc-kr').
    • 드론 csv: UTF-8(BOM). POI/측점/center: EUC-KR.
    • 서버처럼 자동판별(iconv)이 아니라 파일 종류별로 인코딩 지정(폴더 구조 알고 있으므로 단순). center/POI/측점은 EUC-KR 고정.
  • readCsv(file: File, encoding)Promise<string[][]>await file.arrayBuffer() → decode → split(/\r?\n/).filter(Boolean).map(parseCsvLine).

로더 (서버 loader 포팅, but 파일은 File[]에서 매칭)

  • findVideoFile(files) — 확장자 .mp4/.webm(대소문자 무시), building/ 미포함. → base 추출.
  • parseDroneFrames(file): Promise<DroneFrame[]> — 서버 loadFrames:289-302 로직(헤더 이름 인덱스). 회덕 필터 제거, base 일치 또는 building/ 미포함 루트 csv로 식별.
  • parsePois(poiFile, stationFile): Promise<GeoPoint[]> — 서버 loadPois:321-357 포팅. POI=category_clean||'건물',type:'poi'; 측점=category:'측점',type:'station'. 파일 선택은 building/webkitRelativePath에서 POI+위경도, 측점+위경도 매칭(타원체고 우선규칙 유지).
  • parseCenterline(file): Promise<CenterlinePoint[]> — 서버 loadCenterline:253-257 포팅(인덱스 1/2/5, EUC-KR).
  • loadFolderGeoData(files: File[]) — 위 파서들을 호출해 geoStore.setGeoData({...}) 채우고, 영상 File을 반환.

검색 함수 포팅 (서버 geoMatch.ts → 클라이언트)

검색은 서버의 단순 ENU project3D(geoMatch.ts) 알고리즘을 그대로 포팅해 서버 응답과 동일 결과를 보장한다(오버레이용 geoProjection.ts와는 별개 — 검색 UI 호환 우선).

포팅 함수 서버 소스 라인 비고
toRad geoMatch.ts:63
geoToEnu (cos-lat ENU) geoMatch.ts:78-87 검색 전용. 오버레이는 기존 proj4 ENU 사용
projectEnu geoMatch.ts:90-124
project3D geoMatch.ts:127-156
stationOrder geoMatch.ts:371-375 (이미 컴포넌트들에 중복 존재 — 공용화)
getWorldOrigin geoMatch.ts:378-387 스토어 origin으로 대체 가능
findFramesForPoi(query, margin, maxDist, yawOffset) geoMatch.ts:399-468 스토어 frames/pois 입력으로 변경
findPoisForFrame(frameNum, margin, yawOffset) geoMatch.ts:473-512 동일
  • 입력을 싱글턴 캐시(loadFrames()) 대신 geoStore 데이터를 인자/직접 read로 변경. 반환 타입은 서버와 동일(FrameMatch/PoiInFrame) → 컴포넌트 인터페이스 무변경.
  • DEFAULT_FPS=30, GAP=30 등 상수 동일 포팅.

Changes: 소비 컴포넌트 (fetch → 스토어)

각 컴포넌트는 내부 interface GeoPoint/FrameMatch 중복 선언을 types/geo.ts로 통합(선택, 최소변경 시 유지 가능). 핵심은 fetch 제거:

  • GeoSearch.tsx
    • :61 fetch('/api/geo/pois')useGeoStore[...stations, ...pois].
    • :82 /api/geo/search → 포팅 findFramesForPoi(q, 1.0, 1500, 0) 직접 호출(동기). loading/error는 데이터 없을 때만 의미.
    • :98 /api/geo/frame/{n} → 포팅 findPoisForFrame(currentFrame, 1.0).
  • StationVerify.tsx
    • :57 pois → 스토어 stations.
    • :69 frames({frame,lat,lon}) → 스토어 frames(필드 일부만 사용).
    • :102 search → 포팅 findFramesForPoi(title, 1.2, 2000).
  • StationOverlay.tsx
    • :186 pois → 스토어 stations/pois(이미 station/poi 분리 사용). updateWorldOrigin은 스토어 origin으로 대체.
    • :197 centerline → 스토어 centerline.
    • :206 frames → 스토어 frames.
    • 투영은 기존 geoProjection.ts 그대로(변경 없음).
  • RoutePanel.tsx
    • :75 pois → 스토어. :87 frames → 스토어.
  • 폴백/가드: 스토어 loaded===false이면 빈 배열로 동작(현재 fetch 실패 시와 동일한 빈 상태). useEffect[visible] 의존성은 스토어 구독으로 대체(데이터 도착 시 자동 재렌더).
    • (옵션) 스토어가 비어 있을 때 기존 /api/geo/*로 폴백하려면 각 컴포넌트에 if (store.loaded) useStore else fetch 분기. 기본 구현은 스토어 단일 소스 권장(서버 의존 제거가 목표).

영상 재생 배선

  • loadFolderGeoData가 반환한 영상 FileplayerRef.loadLocalFile(file)(기존 경로 그대로).
  • 영상이 폴더에 없으면 지리정보만 로드(오버레이는 동작하나 재생 화면 없음) — 에러 토스트/콘솔 경고.
  • App.tsx: 폴더 선택 핸들러를 VideoPlayer로 내려보내거나 VideoPlayer 내부에서 loadLocalFile(이미 보유) + loadFolderGeoData 직접 호출. VideoPlayer 내부 처리 권장(이미 loadLocalFile 접근 가능).

구현 태스크 목록

  • client/src/types/geo.ts 신규 — DroneFrame/GeoPoint/CenterlinePoint/FrameMatch/PoiInFrame 공용 타입.
  • client/src/store/geoStore.ts 신규 — Zustand 스토어(frames/pois/stations/centerline/origin/baseName/loaded + setGeoData/reset).
  • client/src/utils/geoData.ts 신규:
    • parseCsvLine 포팅(server geoMatch.ts:162-173).
    • decodeBytes/readCsv(TextDecoder utf-8 + euc-kr, BOM 제거).
    • findVideoFile(files) (mp4/MP4/webm, building 제외, base 추출).
    • parseDroneFrames(헤더 인덱스, 회덕 하드코딩 없음).
    • parsePois(POI: category_clean||건물 / 측점: '측점', 타원체고 우선규칙).
    • parseCenterline(인덱스 1/2/5, EUC-KR).
    • loadFolderGeoData(files) → setGeoData + return videoFile.
  • client/src/utils/geoSearch.ts 신규(또는 geoData.ts 내) — toRad/geoToEnu/projectEnu/project3D/stationOrder/findFramesForPoi/findPoisForFrame 포팅(server geoMatch.ts). 입력은 스토어 데이터.
  • client/src/types/dom.d.ts (또는 vite-env.d.ts) — HTMLInputElement webkitdirectory/directory 보강.
  • client/src/components/player/VideoPlayer.tsx — "폴더 선택" UI 추가 + onSelectFolderloadFolderGeoData + 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로 교체. 두 fetch useEffect 제거.
    • precompute/세그먼트 effect 의존성 framesVersionstoreFrames. 데이터 비었을 때 setReady(false)로 idle 복귀.
    • npm run build -w client 통과 확인. 클라이언트 전체에 live /api/geo fetch 0건.
  • 교훈: 다음 탐색 시 grep -rn "fetch(.*\/api\/geo"소비처 전수 조사를 먼저 수행해 누락 방지.