- frontend/ 폴더로 프론트엔드 전체 이관 - signal-batch API 연동 (한국 선박 위치 데이터) - Tailwind CSS 4 + CSS 변수 테마 토큰 (dark/light) - i18next 다국어 (ko/en) 인프라 + 28개 컴포넌트 적용 - 레이어 패널 트리 구조 재설계 (카테고리별 온/오프, 범례) - Google OAuth 로그인 화면 + DEV LOGIN 우회 - 외부 API CORS 프록시 전환 (Airplanes.live, OpenSky, CelesTrak) - ShipLayer 이미지 탭 전환 (signal-batch / MarineTraffic) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
2.2 KiB
TypeScript
86 lines
2.2 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
import type { ReplayState } from '../types';
|
|
import { REPLAY_START, REPLAY_END } from '../data/sampleData';
|
|
|
|
const TICK_INTERVAL = 200; // ms between updates
|
|
const TIME_STEP = 240_000; // 4 minutes of replay time per tick at 1x speed (same visual speed)
|
|
|
|
export function useReplay() {
|
|
const [state, setState] = useState<ReplayState>({
|
|
isPlaying: false,
|
|
currentTime: REPLAY_START,
|
|
startTime: REPLAY_START,
|
|
endTime: REPLAY_END,
|
|
speed: 1,
|
|
});
|
|
|
|
const intervalRef = useRef<number | null>(null);
|
|
|
|
const stop = useCallback(() => {
|
|
if (intervalRef.current !== null) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const play = useCallback(() => {
|
|
setState(prev => ({ ...prev, isPlaying: true }));
|
|
}, []);
|
|
|
|
const pause = useCallback(() => {
|
|
stop();
|
|
setState(prev => ({ ...prev, isPlaying: false }));
|
|
}, [stop]);
|
|
|
|
const seek = useCallback((time: number) => {
|
|
setState(prev => ({
|
|
...prev,
|
|
currentTime: Math.max(prev.startTime, Math.min(prev.endTime, time)),
|
|
}));
|
|
}, []);
|
|
|
|
const setSpeed = useCallback((speed: number) => {
|
|
setState(prev => ({ ...prev, speed }));
|
|
}, []);
|
|
|
|
const reset = useCallback(() => {
|
|
stop();
|
|
setState(prev => ({
|
|
...prev,
|
|
isPlaying: false,
|
|
currentTime: prev.startTime,
|
|
}));
|
|
}, [stop]);
|
|
|
|
const setRange = useCallback((start: number, end: number) => {
|
|
stop();
|
|
setState(prev => ({
|
|
...prev,
|
|
isPlaying: false,
|
|
startTime: start,
|
|
endTime: end,
|
|
currentTime: Math.max(start, Math.min(end, prev.currentTime)),
|
|
}));
|
|
}, [stop]);
|
|
|
|
useEffect(() => {
|
|
if (state.isPlaying) {
|
|
stop();
|
|
intervalRef.current = window.setInterval(() => {
|
|
setState(prev => {
|
|
const next = prev.currentTime + TIME_STEP * prev.speed;
|
|
if (next >= prev.endTime) {
|
|
return { ...prev, currentTime: prev.endTime, isPlaying: false };
|
|
}
|
|
return { ...prev, currentTime: next };
|
|
});
|
|
}, TICK_INTERVAL);
|
|
} else {
|
|
stop();
|
|
}
|
|
return stop;
|
|
}, [state.isPlaying, state.speed, stop]);
|
|
|
|
return { state, play, pause, seek, setSpeed, reset, setRange };
|
|
}
|