signal-batch/frontend/src/features/area-search/components/TimelineControl.tsx
htlee 1cc25f9f3b feat: 다중구역이동 항적 분석 + STS 접촉 분석 프론트엔드 이관
- 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>
2026-02-20 17:07:14 +09:00

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="정지"
>
&#9632;
</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="반복"
>
&#x21BB;
</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>
)
}