- 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>
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
ReferenceLine,
|
|
} from 'recharts';
|
|
import type { SensorLog } from '../types';
|
|
|
|
interface Props {
|
|
data: SensorLog[];
|
|
currentTime: number;
|
|
startTime: number;
|
|
endTime: number;
|
|
}
|
|
|
|
export function SensorChart({ data, currentTime, startTime }: Props) {
|
|
const { t } = useTranslation();
|
|
|
|
const visibleData = useMemo(
|
|
() => data.filter(d => d.timestamp <= currentTime),
|
|
[data, currentTime],
|
|
);
|
|
|
|
const chartData = useMemo(
|
|
() =>
|
|
visibleData.map(d => ({
|
|
...d,
|
|
time: formatHour(d.timestamp, startTime),
|
|
})),
|
|
[visibleData, startTime],
|
|
);
|
|
|
|
return (
|
|
<div className="sensor-chart">
|
|
<h3>{t('sensor.title')}</h3>
|
|
<div className="chart-grid">
|
|
<div className="chart-item">
|
|
<h4>{t('sensor.seismicActivity')}</h4>
|
|
<ResponsiveContainer width="100%" height={80}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
|
<XAxis dataKey="time" tick={{ fontSize: 10, fill: '#888' }} />
|
|
<YAxis domain={[0, 100]} tick={{ fontSize: 10, fill: '#888' }} />
|
|
<Tooltip contentStyle={{ background: '#1a1a2e', border: '1px solid #333' }} />
|
|
<ReferenceLine x={formatHour(currentTime, startTime)} stroke="#fff" strokeDasharray="3 3" />
|
|
<Line type="monotone" dataKey="seismic" stroke="#ef4444" dot={false} strokeWidth={1.5} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
<div className="chart-item">
|
|
<h4>{t('sensor.noiseLevelDb')}</h4>
|
|
<ResponsiveContainer width="100%" height={80}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
|
<XAxis dataKey="time" tick={{ fontSize: 10, fill: '#888' }} />
|
|
<YAxis domain={[0, 140]} tick={{ fontSize: 10, fill: '#888' }} />
|
|
<Tooltip contentStyle={{ background: '#1a1a2e', border: '1px solid #333' }} />
|
|
<Line type="monotone" dataKey="noiseLevel" stroke="#f97316" dot={false} strokeWidth={1.5} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
<div className="chart-item">
|
|
<h4>{t('sensor.airPressureHpa')}</h4>
|
|
<ResponsiveContainer width="100%" height={80}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
|
<XAxis dataKey="time" tick={{ fontSize: 10, fill: '#888' }} />
|
|
<YAxis domain={[990, 1020]} tick={{ fontSize: 10, fill: '#888' }} />
|
|
<Tooltip contentStyle={{ background: '#1a1a2e', border: '1px solid #333' }} />
|
|
<Line type="monotone" dataKey="airPressure" stroke="#3b82f6" dot={false} strokeWidth={1.5} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
<div className="chart-item">
|
|
<h4>{t('sensor.radiationUsv')}</h4>
|
|
<ResponsiveContainer width="100%" height={80}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
|
<XAxis dataKey="time" tick={{ fontSize: 10, fill: '#888' }} />
|
|
<YAxis domain={[0, 0.3]} tick={{ fontSize: 10, fill: '#888' }} />
|
|
<Tooltip contentStyle={{ background: '#1a1a2e', border: '1px solid #333' }} />
|
|
<Line type="monotone" dataKey="radiationLevel" stroke="#22c55e" dot={false} strokeWidth={1.5} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatHour(timestamp: number, startTime: number): string {
|
|
const hours = (timestamp - startTime) / 3600_000;
|
|
const h = Math.floor(hours);
|
|
const m = Math.round((hours - h) * 60);
|
|
return `${h}:${m.toString().padStart(2, '0')}`;
|
|
}
|