/** * 드론 지리정보 검색 패널 * - 건물/측점명 → 해당 프레임 탐색 * - 현재 프레임 → 보이는 건물/측점 목록 */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useGeoStore } from '../../store/geoStore'; import { findFramesForPoi, findPoisForFrame } from '../../utils/geoSearch'; import type { GeoPoint, FrameMatch, PoiInFrame } from '../../types/geo'; interface Props { currentFrame: number; fps: number; onSeekToFrame: (frame: number) => void; } type Tab = 'search' | 'reverse'; export default function GeoSearch({ currentFrame, fps, onSeekToFrame }: Props) { const [tab, setTab] = useState('search'); const [query, setQuery] = useState(''); const [suggestions, setSuggestions] = useState([]); const [searchResult, setSearchResult] = useState<{ poi: GeoPoint; frames: FrameMatch[] } | null>(null); const [reverseResult, setReverseResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const debounceRef = useRef | null>(null); // 클라이언트 지리정보 스토어 구독 (서버 /api/geo/* 대체) const loaded = useGeoStore(s => s.loaded); const frames = useGeoStore(s => s.frames); const pois = useGeoStore(s => s.pois); const stations = useGeoStore(s => s.stations); // POI 목록 (자동완성용): 측점 + 건물 통합 (서버 /api/geo/pois 동치) const allPois = React.useMemo( () => (loaded ? [...stations, ...pois] : []), [loaded, stations, pois], ); // 자동완성 필터링 useEffect(() => { if (!query.trim()) { setSuggestions([]); return; } const q = query.toLowerCase(); setSuggestions(allPois.filter(p => p.title.toLowerCase().includes(q)).slice(0, 10)); }, [query, allPois]); // 건물/측점명으로 프레임 검색 (클라이언트 검색 — 서버 /api/geo/search 대체) const handleSearch = useCallback((q?: string) => { const searchQ = (q ?? query).trim(); if (!searchQ) return; setLoading(true); setError(''); setSuggestions([]); try { if (!loaded) { setError('폴더를 먼저 선택하세요'); setSearchResult(null); return; } const origin = useGeoStore.getState().origin; const combined = [...stations, ...pois]; const result = findFramesForPoi(frames, combined, searchQ, 1.0, 1500, 0, origin); if (!result.poi) { setError('일치하는 건물/측점 없음'); setSearchResult(null); return; } setSearchResult({ poi: result.poi, frames: result.frames }); } finally { setLoading(false); } }, [query, loaded, frames, stations, pois]); // 현재 프레임 역조회 (클라이언트 검색 — 서버 /api/geo/frame/{n} 대체) const handleReverse = useCallback(() => { setLoading(true); setError(''); try { if (!loaded) { setReverseResult([]); return; } const origin = useGeoStore.getState().origin; const combined = [...stations, ...pois]; const result = findPoisForFrame(frames, combined, currentFrame, 1.0, 0, origin); setReverseResult(result.pois); } finally { setLoading(false); } }, [currentFrame, loaded, frames, stations, pois]); // 탭 전환/프레임 변경/데이터 로드 시 역조회 자동 실행 useEffect(() => { if (tab === 'reverse') handleReverse(); }, [tab, currentFrame, handleReverse]); const formatDist = (m: number) => m >= 1000 ? `${(m / 1000).toFixed(2)}km` : `${Math.round(m)}m`; const formatAngle = (deg: number) => `${deg >= 0 ? '+' : ''}${deg.toFixed(1)}°`; return (
{/* 탭 */}
{/* 검색 탭 */} {tab === 'search' && (
setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} />
{/* 자동완성 */} {suggestions.length > 0 && (
{suggestions.map((s, i) => ( ))}
)}
{error &&
{error}
} {searchResult && (
{/* POI 정보 */}
{searchResult.poi.title}
{searchResult.poi.category} · 표고 {searchResult.poi.z.toFixed(1)}m
{searchResult.poi.lat.toFixed(6)}, {searchResult.poi.lon.toFixed(6)}
{searchResult.frames.length}개 관측 구간
{searchResult.frames.length === 0 && (
카메라 시야에 들어오는 프레임 없음
)}
{searchResult.frames.map((fm, i) => ( ))}
)}
)} {/* 역조회 탭 */} {tab === 'reverse' && (
현재 프레임: #{currentFrame}
{error &&
{error}
}
{reverseResult && reverseResult.length === 0 && (
현재 프레임 시야에 건물/측점 없음
)} {reverseResult && reverseResult.length > 0 && (
{reverseResult.map((item, i) => (
{item.poi.title} {formatDist(item.distance)}
{item.poi.type === 'station' ? '측점' : item.poi.category} 수평 {formatAngle(item.bearingDiff)} / 수직 {formatAngle(item.elevationDiff)}
화면 위치 ({(item.pixelX * 100).toFixed(0)}%, {(item.pixelY * 100).toFixed(0)}%)
))}
)}
)}
); }