kcg-monitoring/frontend/src/components/SensorChart.tsx
htlee 2534faa488 feat: 프론트엔드 모노레포 이관 + signal-batch 연동 + Tailwind/i18n/테마 전환
- 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>
2026-03-17 13:54:41 +09:00

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')}`;
}