kcg-monitoring/src/components/ReplayControls.tsx
htlee ccdfb3517b feat: KCG 모니터링 대시보드 초기 프로젝트 구성
React 19 + TypeScript + Vite + MapLibre 기반 해양 모니터링 대시보드.
선박 AIS, 항공기, CCTV, 위성, 해양 인프라 등 다중 레이어 지원.
ESLint React Compiler 규칙 조정 및 lint 에러 수정 포함.
2026-03-17 09:01:18 +09:00

169 lines
5.3 KiB
TypeScript

import { useState, useCallback } from 'react';
interface Props {
isPlaying: boolean;
speed: number;
startTime: number;
endTime: number;
onPlay: () => void;
onPause: () => void;
onReset: () => void;
onSpeedChange: (speed: number) => void;
onRangeChange: (start: number, end: number) => void;
}
const SPEEDS = [1, 2, 4, 8, 16];
// Preset ranges relative to T0 (main strike moment)
const T0 = new Date('2026-03-01T12:01:00Z').getTime();
const HOUR = 3600_000;
const PRESETS = [
{ label: '24H', start: T0 - 12 * HOUR, end: T0 + 12 * HOUR },
{ label: '12H', start: T0 - 6 * HOUR, end: T0 + 6 * HOUR },
{ label: '6H', start: T0 - 3 * HOUR, end: T0 + 3 * HOUR },
{ label: '2H', start: T0 - HOUR, end: T0 + HOUR },
{ label: '30M', start: T0 - 15 * 60_000, end: T0 + 15 * 60_000 },
];
const KST_OFFSET = 9 * 3600_000; // KST = UTC+9
function toKSTInput(ts: number): string {
// Format as datetime-local value in KST: YYYY-MM-DDTHH:MM
const d = new Date(ts + KST_OFFSET);
const pad = (n: number) => n.toString().padStart(2, '0');
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
}
function fromKSTInput(val: string): number {
// Parse datetime-local as KST → convert to UTC
return new Date(val + 'Z').getTime() - KST_OFFSET;
}
export function ReplayControls({
isPlaying,
speed,
startTime,
endTime,
onPlay,
onPause,
onReset,
onSpeedChange,
onRangeChange,
}: Props) {
const [showPicker, setShowPicker] = useState(false);
const [customStart, setCustomStart] = useState(toKSTInput(startTime));
const [customEnd, setCustomEnd] = useState(toKSTInput(endTime));
const handlePreset = useCallback((preset: typeof PRESETS[number]) => {
onRangeChange(preset.start, preset.end);
setCustomStart(toKSTInput(preset.start));
setCustomEnd(toKSTInput(preset.end));
}, [onRangeChange]);
const handleCustomApply = useCallback(() => {
const s = fromKSTInput(customStart);
const e = fromKSTInput(customEnd);
if (s < e) {
onRangeChange(s, e);
setShowPicker(false);
}
}, [customStart, customEnd, onRangeChange]);
// Find which preset is active
const activePreset = PRESETS.find(p => p.start === startTime && p.end === endTime);
return (
<div className="replay-controls">
{/* Left: transport controls */}
<button className="ctrl-btn" onClick={onReset} title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M1 4v6h6" />
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
</svg>
</button>
<button className="ctrl-btn play-btn" onClick={isPlaying ? onPause : onPlay}>
{isPlaying ? (
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
) : (
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5,3 19,12 5,21" />
</svg>
)}
</button>
<div className="speed-controls">
{SPEEDS.map(s => (
<button
key={s}
className={`speed-btn ${speed === s ? 'active' : ''}`}
onClick={() => onSpeedChange(s)}
>
{s}x
</button>
))}
</div>
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* Right: range presets + custom picker */}
<div className="range-controls">
<div className="range-presets">
{PRESETS.map(p => (
<button
key={p.label}
className={`range-btn ${activePreset === p ? 'active' : ''}`}
onClick={() => handlePreset(p)}
>
{p.label}
</button>
))}
<button
className={`range-btn custom-btn ${showPicker ? 'active' : ''}`}
onClick={() => setShowPicker(!showPicker)}
title="Custom range"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
</button>
</div>
{showPicker && (
<div className="range-picker">
<div className="range-picker-row">
<label>
<span>FROM (KST)</span>
<input
type="datetime-local"
value={customStart}
onChange={e => setCustomStart(e.target.value)}
/>
</label>
<label>
<span>TO (KST)</span>
<input
type="datetime-local"
value={customEnd}
onChange={e => setCustomEnd(e.target.value)}
/>
</label>
<button className="range-apply-btn" onClick={handleCustomApply}>
APPLY
</button>
</div>
</div>
)}
</div>
</div>
);
}