Files
GhiVideo/client/src/store/geoStore.ts
T
b23042andClaude Opus 4.8 78fc090360 초기 커밋: 스테이션(측점) 기반 주행영상 플레이어
- 클라이언트(React/Vite): Video.js 플레이어, 하단 스테이션바, POI/구조물 영상 오버레이, 카메라 파라미터 보정, 미니맵/RoutePanel
- 서버(Express): Range 스트리밍, HLS 변환, 프레임 추출, tus 업로드, 주석 API
- 최근 작업: 스테이션바 종점역 표출/양끝 정렬/미도착(재생불가) 표시, POI 라벨 '구분' 우선·컴팩트 팝업·겹침제외 토글·동일좌표 다중행, 진행방향(상/하) 우선 표출, 커서 배지 크기·픽셀 떨림 개선

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:37:57 +09:00

114 lines
3.7 KiB
TypeScript

/**
* 클라이언트 지리정보 스토어 (Zustand)
*
* 폴더 선택으로 파싱한 드론 프레임 / POI / 측점 / 중심선 / ENU 원점을 보관한다.
* 4개 소비 컴포넌트(GeoSearch / StationVerify / StationOverlay / RoutePanel)가
* 이 스토어를 단일 소스로 구독한다(서버 /api/geo/* 대체).
*
* playerStore.ts 패턴을 따른다.
*
* POI 위치 보정(poiOverrides): 마우스 드래그로 만든 title→{lat,lon,z} 맵.
* basePois(파싱 원본)에 applyPoiOverrides 를 적용한 결과가 pois 다.
* 내보내기/가져오기 파일(`<base>_poi_overrides.json`)로 영속화한다.
*/
import { create } from 'zustand';
import type {
DroneFrame,
GeoPoint,
CenterlinePoint,
GeoOrigin,
RouteMeta,
RouteStructure,
DirectionChange,
PoiOverride,
PoiOverrideMap,
} from '../types/geo';
import { loadFolderGeoData, applyPoiOverrides } from '../utils/geoData';
interface GeoStore {
loaded: boolean;
frames: DroneFrame[];
pois: GeoPoint[]; // type==='poi' (poiOverrides 적용 결과)
basePois: GeoPoint[]; // 파싱 원본 (보정 전) — 보정 재적용용
poiOverrides: PoiOverrideMap; // title → {lat,lon,z}
stations: GeoPoint[]; // type==='station' (stationOrder 정렬)
centerline: CenterlinePoint[];
origin: GeoOrigin | null;
baseName: string | null;
routeMeta: RouteMeta | null;
/** v2.0 CSV(03)교량/04)터널/06)구교) 유래 구조물 + route.json 보정. */
structures: RouteStructure[];
/** 측점 비고에서 추출한 방향전환점 목록. */
directionChanges: DirectionChange[];
/**
* 폴더 선택 파일에서 지리정보를 파싱해 스토어에 적재한다.
* 발견한 영상 File 을 반환(없으면 null) — 호출자가 loadLocalFile 로 재생.
*/
loadFromFolder: (files: FileList | File[]) => Promise<File | null>;
/** 단일 POI 보정 설정/갱신 (드래그 종료 시). title 키. */
setPoiOverride: (title: string, ov: PoiOverride) => void;
/** 단일 POI 보정 해제. */
clearPoiOverride: (title: string) => void;
/** 보정맵 전체 교체 (가져오기). */
setPoiOverrides: (map: PoiOverrideMap) => void;
clear: () => void;
}
const EMPTY = {
loaded: false,
frames: [] as DroneFrame[],
pois: [] as GeoPoint[],
basePois: [] as GeoPoint[],
poiOverrides: {} as PoiOverrideMap,
stations: [] as GeoPoint[],
centerline: [] as CenterlinePoint[],
origin: null as GeoOrigin | null,
baseName: null as string | null,
routeMeta: null as RouteMeta | null,
structures: [] as RouteStructure[],
directionChanges: [] as DirectionChange[],
};
export const useGeoStore = create<GeoStore>((set) => ({
...EMPTY,
loadFromFolder: async (files) => {
const data = await loadFolderGeoData(files);
set({
loaded: true,
frames: data.frames,
basePois: data.pois,
poiOverrides: data.poiOverrides,
pois: applyPoiOverrides(data.pois, data.poiOverrides),
stations: data.stations,
centerline: data.centerline,
origin: data.origin,
baseName: data.baseName,
routeMeta: data.routeMeta,
structures: data.structures,
directionChanges: data.directionChanges,
});
return data.videoFile;
},
setPoiOverride: (title, ov) =>
set((s) => {
const poiOverrides = { ...s.poiOverrides, [title]: ov };
return { poiOverrides, pois: applyPoiOverrides(s.basePois, poiOverrides) };
}),
clearPoiOverride: (title) =>
set((s) => {
const poiOverrides = { ...s.poiOverrides };
delete poiOverrides[title];
return { poiOverrides, pois: applyPoiOverrides(s.basePois, poiOverrides) };
}),
setPoiOverrides: (map) =>
set((s) => ({ poiOverrides: map, pois: applyPoiOverrides(s.basePois, map) })),
clear: () => set({ ...EMPTY }),
}));