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:
htlee 2026-03-23 10:58:00 +09:00
부모 2b009ca81a
커밋 03f659986f
3개의 변경된 파일73개의 추가작업 그리고 1개의 파일을 삭제

파일 보기

@ -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" />

파일 보기

@ -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]);
}

파일 보기

@ -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;
}
}