diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 3701952..acee348 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -142,6 +142,14 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF const [selectedGearData, setSelectedGearData] = useState(null); const [selectedFleetData, setSelectedFleetData] = useState(null); const [zoomLevel, setZoomLevel] = useState(KOREA_MAP_ZOOM); + const zoomRef = useRef(KOREA_MAP_ZOOM); + const handleZoom = useCallback((e: { viewState: { zoom: number } }) => { + const z = Math.floor(e.viewState.zoom); + if (z !== zoomRef.current) { + zoomRef.current = z; + setZoomLevel(z); + } + }, []); const [staticPickInfo, setStaticPickInfo] = useState(null); useEffect(() => { @@ -480,7 +488,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF initialViewState={{ ...KOREA_MAP_CENTER, zoom: KOREA_MAP_ZOOM }} style={{ width: '100%', height: '100%' }} mapStyle={MAP_STYLE} - onZoom={e => setZoomLevel(Math.floor(e.viewState.zoom))} + onZoom={handleZoom} > diff --git a/frontend/src/hooks/usePoll.ts b/frontend/src/hooks/usePoll.ts new file mode 100644 index 0000000..257e497 --- /dev/null +++ b/frontend/src/hooks/usePoll.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef, useCallback } from 'react'; + +/** + * 공통 폴링 훅 — 주기적으로 fetchFn을 호출하고 결과를 onData로 전달. + * enabled가 false면 폴링 중지. + */ +export function usePoll( + fetchFn: () => Promise, + onData: (data: T) => void, + intervalMs: number, + enabled = true, +): void { + const onDataRef = useRef(onData); + onDataRef.current = onData; + + const fetchRef = useRef(fetchFn); + fetchRef.current = fetchFn; + + const doFetch = useCallback(async () => { + try { + const data = await fetchRef.current(); + onDataRef.current(data); + } catch { + // graceful — 기존 데이터 유지 + } + }, []); + + useEffect(() => { + if (!enabled) return; + doFetch(); + const t = setInterval(doFetch, intervalMs); + return () => clearInterval(t); + }, [enabled, intervalMs, doFetch]); +} diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts new file mode 100644 index 0000000..abcbd2b --- /dev/null +++ b/frontend/src/services/apiClient.ts @@ -0,0 +1,30 @@ +const BASE_PREFIX = '/api/kcg'; + +/** + * KCG 백엔드 API 호출 래퍼. + * - 자동 credentials: 'include' + * - JSON 파싱 + * - 에러 시 null 반환 (graceful degradation) + */ +export async function kcgFetch(path: string): Promise { + try { + const res = await fetch(`${BASE_PREFIX}${path}`, { credentials: 'include' }); + if (!res.ok) return null; + return await res.json() as T; + } catch { + return null; + } +} + +/** + * 외부 API 호출 래퍼 (CORS 프록시 경유). + */ +export async function externalFetch(url: string): Promise { + try { + const res = await fetch(url); + if (!res.ok) return null; + return await res.json() as T; + } catch { + return null; + } +}