refactor: Phase 5+6 — 줌 디바운싱 + API 클라이언트 + 폴링 유틸
Phase 5 (렌더링 최적화): - KoreaMap: onZoom ref 기반 비교로 불필요한 setState 방지 Phase 6 (서비스 정리): - apiClient.ts: kcgFetch/externalFetch 래퍼 (credentials, error handling) - usePoll.ts: 공통 폴링 훅 (interval + enabled + graceful error)
This commit is contained in:
부모
2b009ca81a
커밋
03f659986f
@ -142,6 +142,14 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
const [selectedGearData, setSelectedGearData] = useState<SelectedGearGroupData | null>(null);
|
||||
const [selectedFleetData, setSelectedFleetData] = useState<SelectedFleetData | null>(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<StaticPickInfo | null>(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}
|
||||
>
|
||||
<NavigationControl position="top-right" />
|
||||
|
||||
|
||||
34
frontend/src/hooks/usePoll.ts
Normal file
34
frontend/src/hooks/usePoll.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 공통 폴링 훅 — 주기적으로 fetchFn을 호출하고 결과를 onData로 전달.
|
||||
* enabled가 false면 폴링 중지.
|
||||
*/
|
||||
export function usePoll<T>(
|
||||
fetchFn: () => Promise<T>,
|
||||
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]);
|
||||
}
|
||||
30
frontend/src/services/apiClient.ts
Normal file
30
frontend/src/services/apiClient.ts
Normal file
@ -0,0 +1,30 @@
|
||||
const BASE_PREFIX = '/api/kcg';
|
||||
|
||||
/**
|
||||
* KCG 백엔드 API 호출 래퍼.
|
||||
* - 자동 credentials: 'include'
|
||||
* - JSON 파싱
|
||||
* - 에러 시 null 반환 (graceful degradation)
|
||||
*/
|
||||
export async function kcgFetch<T>(path: string): Promise<T | null> {
|
||||
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<T>(url: string): Promise<T | null> {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
return await res.json() as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user