feat(map): AIS 로딩 인디케이터 및 UX 개선 #6

병합
htlee feat/ais-loading-and-ux 에서 develop 로 2 commits 를 머지했습니다 2026-03-31 15:31:57 +09:00
5개의 변경된 파일37개의 추가작업 그리고 9개의 파일을 삭제
Showing only changes of commit 0a4850d3f4 - Show all commits

파일 보기

@ -63,11 +63,11 @@ body {
/* deck.gl tooltip */
.deck-tooltip{
background:rgba(26,58,74,.96)!important;
color:#fff!important;
border:1px solid rgba(0,147,178,.35)!important;
background:rgba(255,255,255,.97)!important;
color:#1a2e38!important;
border:1px solid #d4dde2!important;
border-radius:6px!important;
box-shadow:0 6px 20px rgba(0,0,0,.35)!important;
box-shadow:0 4px 16px rgba(0,0,0,.15)!important;
padding:7px 9px!important;
font-size:12px!important;
line-height:1.35!important;

파일 보기

@ -30,6 +30,8 @@ const MapViewML: React.FC<MapViewMLProps> = ({ children }) => {
const [scaleText, setScaleText] = React.useState('1:50,000');
const [zoom, setZoom] = React.useState(10);
const aisLoading = useStore((s) => s.aisLoading);
const aisEmpty = useStore((s) => s.aisTargets.size === 0);
const photoModal = useStore((s) => s.photoModal);
const openPhotoModal = useStore((s) => s.openPhotoModal);
const closePhotoModal = useStore((s) => s.closePhotoModal);
@ -97,6 +99,20 @@ const MapViewML: React.FC<MapViewMLProps> = ({ children }) => {
<ScaleBar text={`${scaleText} ━━━ 5 km`} />
<MapLegend />
{children}
{(aisLoading || aisEmpty) && (
<>
<div style={{ position: 'absolute', inset: 0, background: 'rgba(255,255,255,.45)', zIndex: 15, pointerEvents: 'none' }} />
<div style={{ position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', background: 'rgba(26,58,74,.88)', color: '#fff', padding: '8px 20px', borderRadius: 6, fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 10, zIndex: 20, boxShadow: '0 2px 8px rgba(0,0,0,.25)', pointerEvents: 'none' }}>
<svg width="18" height="18" viewBox="0 0 24 24" style={{ animation: 'spin 1s linear infinite' }}>
<circle cx="12" cy="12" r="10" fill="none" stroke="rgba(255,255,255,.3)" strokeWidth="3" />
<path d="M12 2a10 10 0 0 1 10 10" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" />
</svg>
...
</div>
</>
)}
<CoordStatusBar coord={coord} zoom={zoom} scale={scaleText} />
{photoModal && (

파일 보기

@ -10,6 +10,7 @@ const RETENTION_MS = 120 * 60 * 1000;
export function useAisPolling() {
const setAisTargets = useStore((s) => s.setAisTargets);
const setAisLoading = useStore((s) => s.setAisLoading);
const storeRef = useRef(useStore.getState);
useEffect(() => {
@ -19,6 +20,7 @@ export function useAisPolling() {
useEffect(() => {
const ac = new AbortController();
let timer: ReturnType<typeof setInterval> | null = null;
let isInitial = true;
function mergeAndPrune(incoming: AisTarget[]) {
const prev = storeRef.current().aisTargets;
@ -43,12 +45,18 @@ export function useAisPolling() {
async function poll(minutes: number) {
try {
if (isInitial) setAisLoading(true);
const targets = await fetchRecentPositions(minutes, ac.signal);
mergeAndPrune(targets);
} catch (err) {
if ((err as Error).name !== 'AbortError') {
console.warn('[AIS] Poll failed:', err);
}
} finally {
if (isInitial) {
setAisLoading(false);
isInitial = false;
}
}
}
@ -61,5 +69,5 @@ export function useAisPolling() {
ac.abort();
if (timer) clearInterval(timer);
};
}, [setAisTargets]);
}, [setAisTargets, setAisLoading]);
}

파일 보기

@ -28,6 +28,8 @@ interface AppState {
// AIS 실시간 데이터
aisTargets: Map<number, AisTarget>;
setAisTargets: (targets: Map<number, AisTarget>) => void;
aisLoading: boolean;
setAisLoading: (loading: boolean) => void;
// 선박 사진 모달
photoModal: { imo: number; name: string; imagePath: string; imageCount: number } | null;
@ -72,6 +74,8 @@ export const useStore = create<AppState>((set) => ({
aisTargets: new Map(),
setAisTargets: (targets) => set({ aisTargets: targets }),
aisLoading: true,
setAisLoading: (loading) => set({ aisLoading: loading }),
photoModal: null,
openPhotoModal: (info) => set({ photoModal: info }),

파일 보기

@ -1,10 +1,10 @@
import type maplibregl from 'maplibre-gl';
/** 인천 해역 기본 중심 좌표 [lng, lat] */
export const DEFAULT_CENTER: [number, number] = [126.7052, 37.4563];
/** 대한민국 전역 중심 좌표 [lng, lat] */
export const DEFAULT_CENTER: [number, number] = [127.8, 35.5];
/** 기본 줌 레벨 */
export const DEFAULT_ZOOM = 10;
/** 기본 줌 레벨 (대한민국 전역 표시) */
export const DEFAULT_ZOOM = 7;
/** Martin ENC 타일 서버 프록시 경로 */
export const MARTIN_BASE_PATH = '/martin';