- terra-draw 기반 지도 폴리곤/사각형/원 그리기 + 편집 (OL Draw 대체) - 구역 항적 분석: ANY/ALL/SEQUENTIAL 검색모드, 다중구역 시각화 - STS 선박쌍 접촉 분석: 접촉쌍 그룹핑, 위험도 indicator, ScatterplotLayer - Deck.gl 레이어: PathLayer + TripsLayer + IconLayer (커서 기반 O(1) 보간) - 공유 타임라인 컨트롤 (재생/배속/프로그레스바) - CSV 내보내기 (다중 방문 동적 컬럼, BOM+UTF-8) - ApiExplorer 5모드 통합 (positions/vessel/replay/area-search/sts) 신규 17파일 (features/area-search/), 수정 5파일 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import { useAreaAnimationStore, PLAYBACK_SPEEDS } from '../stores/animationStore'
|
|
|
|
export default function TimelineControl() {
|
|
const isPlaying = useAreaAnimationStore((s) => s.isPlaying)
|
|
const currentTime = useAreaAnimationStore((s) => s.currentTime)
|
|
const startTime = useAreaAnimationStore((s) => s.startTime)
|
|
const endTime = useAreaAnimationStore((s) => s.endTime)
|
|
const playbackSpeed = useAreaAnimationStore((s) => s.playbackSpeed)
|
|
const loop = useAreaAnimationStore((s) => s.loop)
|
|
const play = useAreaAnimationStore((s) => s.play)
|
|
const pause = useAreaAnimationStore((s) => s.pause)
|
|
const stop = useAreaAnimationStore((s) => s.stop)
|
|
const setPlaybackSpeed = useAreaAnimationStore((s) => s.setPlaybackSpeed)
|
|
const setProgressByRatio = useAreaAnimationStore((s) => s.setProgressByRatio)
|
|
const toggleLoop = useAreaAnimationStore((s) => s.toggleLoop)
|
|
|
|
const duration = endTime - startTime
|
|
const progress = duration > 0 ? (currentTime - startTime) / duration : 0
|
|
|
|
const formatTime = (ms: number) => {
|
|
const d = new Date(ms)
|
|
return d.toLocaleString('ko-KR', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
})
|
|
}
|
|
|
|
if (startTime === 0 && endTime === 0) return null
|
|
|
|
return (
|
|
<div className="absolute bottom-4 left-1/2 z-10 w-[560px] -translate-x-1/2 rounded-lg border border-border bg-surface/95 p-3 shadow-lg backdrop-blur">
|
|
{/* 타임라인 */}
|
|
<div className="mb-2">
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={1000}
|
|
value={Math.round(progress * 1000)}
|
|
onChange={(e) => setProgressByRatio(Number(e.target.value) / 1000)}
|
|
className="h-1.5 w-full cursor-pointer accent-primary"
|
|
/>
|
|
<div className="mt-1 flex justify-between text-[10px] text-muted">
|
|
<span>{formatTime(startTime)}</span>
|
|
<span className="font-medium text-foreground">{formatTime(currentTime)}</span>
|
|
<span>{formatTime(endTime)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 컨트롤 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={stop}
|
|
className="rounded px-2 py-1 text-xs text-muted hover:bg-surface-hover"
|
|
title="정지"
|
|
>
|
|
■
|
|
</button>
|
|
<button
|
|
onClick={isPlaying ? pause : play}
|
|
className="rounded bg-primary px-3 py-1 text-xs font-medium text-white hover:bg-primary/90"
|
|
>
|
|
{isPlaying ? '||' : '\u25B6'}
|
|
</button>
|
|
<button
|
|
onClick={toggleLoop}
|
|
className={`rounded px-2 py-1 text-xs transition ${
|
|
loop ? 'text-primary' : 'text-muted hover:text-foreground'
|
|
}`}
|
|
title="반복"
|
|
>
|
|
↻
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{PLAYBACK_SPEEDS.map((speed) => (
|
|
<button
|
|
key={speed}
|
|
onClick={() => setPlaybackSpeed(speed)}
|
|
className={`rounded px-1.5 py-0.5 text-[10px] transition ${
|
|
playbackSpeed === speed
|
|
? 'bg-primary/10 font-bold text-primary'
|
|
: 'text-muted hover:text-foreground'
|
|
}`}
|
|
>
|
|
{speed}x
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|