wing-ops/frontend/src/tabs/weather/components/WeatherRightPanel.tsx

401 lines
15 KiB
TypeScript
Executable File
Raw Blame 히스토리

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

interface WeatherData {
stationName: string;
location: { lat: number; lon: number };
currentTime: string;
wind: {
speed: number;
direction: number;
directionLabel: string;
speed_1k: number;
speed_3k: number;
};
wave: {
height: number;
maxHeight: number;
period: number;
direction: string;
};
temperature: {
current: number;
feelsLike: number;
};
pressure: number;
visibility: number;
salinity: number;
astronomy?: {
sunrise: string;
sunset: string;
moonrise: string;
moonset: string;
moonPhase: string;
tidalRange: number;
};
alert?: string;
forecast: WeatherForecast[];
}
interface WeatherForecast {
time: string;
hour: string;
icon: string;
temperature: number;
windSpeed: number;
}
interface WeatherRightPanelProps {
weatherData: WeatherData | null;
}
/** 풍속 등급 색상 */
function windColor(speed: number): string {
if (speed >= 14) return '#ef4444';
if (speed >= 10) return '#f97316';
if (speed >= 6) return '#eab308';
return '#22c55e';
}
/** 파고 등급 색상 */
function waveColor(height: number): string {
if (height >= 3) return '#ef4444';
if (height >= 2) return '#f97316';
if (height >= 1) return '#eab308';
return '#22c55e';
}
/** 풍향 텍스트 */
function windDirText(deg: number): string {
const dirs = [
'N',
'NNE',
'NE',
'ENE',
'E',
'ESE',
'SE',
'SSE',
'S',
'SSW',
'SW',
'WSW',
'W',
'WNW',
'NW',
'NNW',
];
return dirs[Math.round(deg / 22.5) % 16];
}
export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
if (!weatherData) {
return (
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden w-[320px] shrink-0">
<div className="p-6 text-center">
<p className="text-fg-disabled text-[13px] font-korean">
</p>
</div>
</div>
);
}
const { wind, wave, temperature, pressure, visibility, salinity, astronomy, alert, forecast } =
weatherData;
const wSpd = wind.speed;
const wHgt = wave.height;
const wTemp = temperature.current;
const windDir = windDirText(wind.direction);
return (
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden w-[320px] shrink-0">
{/* 헤더 */}
<div className="px-4 py-3 border-b border-stroke bg-bg-elevated">
<div className="flex items-center gap-2 mb-1">
<span className="text-[13px] font-bold text-color-accent font-korean">
📍 {weatherData.stationName}
</span>
<span className="px-1.5 py-px text-[11px] rounded bg-color-accent/15 text-color-accent font-bold">
</span>
</div>
<p className="text-[11px] text-fg-disabled font-mono">
{weatherData.location.lat.toFixed(2)}°N, {weatherData.location.lon.toFixed(2)}°E ·{' '}
{weatherData.currentTime}
</p>
</div>
{/* 스크롤 콘텐츠 */}
<div
className="flex-1 overflow-auto"
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}
>
{/* ── 핵심 지표 3칸 카드 ── */}
<div className="grid grid-cols-3 gap-1 px-3 py-2.5">
<div className="text-center py-2.5 bg-bg-base border border-stroke rounded-md">
<div className="text-[20px] font-bold font-mono" style={{ color: windColor(wSpd) }}>
{wSpd.toFixed(1)}
</div>
<div className="text-[11px] text-fg-disabled font-korean"> (m/s)</div>
</div>
<div className="text-center py-2.5 bg-bg-base border border-stroke rounded-md">
<div className="text-[20px] font-bold font-mono" style={{ color: waveColor(wHgt) }}>
{wHgt.toFixed(1)}
</div>
<div className="text-[11px] text-fg-disabled font-korean"> (m)</div>
</div>
<div className="text-center py-2.5 bg-bg-base border border-stroke rounded-md">
<div className="text-[20px] font-bold font-mono text-color-accent">
{wTemp.toFixed(1)}
</div>
<div className="text-[11px] text-fg-disabled font-korean"> (°C)</div>
</div>
</div>
{/* ── 바람 상세 ── */}
<div className="px-3 py-2 border-b border-stroke">
<div className="text-[11px] font-bold text-fg-disabled font-korean mb-2">
🌬
</div>
<div className="flex items-center gap-3 mb-2">
{/* 풍향 컴파스 */}
<div className="relative w-[50px] h-[50px] shrink-0">
<svg viewBox="0 0 50 50" className="w-full h-full">
<circle
cx="25"
cy="25"
r="22"
fill="none"
stroke="var(--stroke-default)"
strokeWidth="1"
/>
<circle
cx="25"
cy="25"
r="16"
fill="none"
stroke="var(--stroke-default)"
strokeWidth="0.5"
strokeDasharray="2 2"
/>
{['N', 'E', 'S', 'W'].map((d, i) => {
const angle = i * 90;
const rad = ((angle - 90) * Math.PI) / 180;
const x = 25 + 20 * Math.cos(rad);
const y = 25 + 20 * Math.sin(rad);
return (
<text
key={d}
x={x}
y={y}
textAnchor="middle"
dominantBaseline="central"
fill="var(--fg-disabled)"
fontSize="6"
fontWeight="bold"
>
{d}
</text>
);
})}
{/* 풍향 화살표 */}
<line
x1="25"
y1="25"
x2={25 + 14 * Math.sin((wind.direction * Math.PI) / 180)}
y2={25 - 14 * Math.cos((wind.direction * Math.PI) / 180)}
stroke={windColor(wSpd)}
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="25" cy="25" r="3" fill={windColor(wSpd)} />
</svg>
</div>
<div className="flex-1 grid grid-cols-2 gap-x-3 gap-y-1.5 text-[11px]">
<div className="flex justify-between">
<span className="text-fg-disabled"></span>
<span className="text-fg font-mono font-bold text-[13px]">
{windDir} {wind.direction}°
</span>
</div>
<div className="flex justify-between">
<span className="text-fg-disabled"></span>
<span className="text-fg font-mono text-[13px]">{pressure} hPa</span>
</div>
<div className="flex justify-between">
<span className="text-fg-disabled">1k </span>
<span className="font-mono text-[13px]" style={{ color: windColor(wind.speed_1k) }}>
{Number(wind.speed_1k).toFixed(1)}
</span>
</div>
<div className="flex justify-between">
<span className="text-fg-disabled">3k </span>
<span className="font-mono text-[13px]" style={{ color: windColor(wind.speed_3k) }}>
{Number(wind.speed_3k).toFixed(1)}
</span>
</div>
<div className="col-span-2 flex justify-between">
<span className="text-fg-disabled"></span>
<span className="text-fg font-mono text-[13px]">{visibility} km</span>
</div>
</div>
</div>
{/* 풍속 게이지 바 */}
<div className="flex items-center gap-2">
<div className="flex-1 h-[5px] bg-bg-card rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${Math.min((wSpd / 20) * 100, 100)}%`,
background: windColor(wSpd),
}}
/>
</div>
<span className="text-[11px] font-mono text-fg-disabled shrink-0">
{wSpd.toFixed(1)}/20
</span>
</div>
</div>
{/* ── 파도 상세 ── */}
<div className="px-3 py-2 border-b border-stroke">
<div className="text-[11px] font-bold text-fg-disabled font-korean mb-2">🌊 </div>
<div className="grid grid-cols-4 gap-1">
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono" style={{ color: waveColor(wHgt) }}>
{wHgt.toFixed(1)}m
</div>
<div className="text-[10px] text-fg-disabled"></div>
</div>
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono text-color-danger">
{wave.maxHeight.toFixed(1)}m
</div>
<div className="text-[10px] text-fg-disabled"></div>
</div>
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono text-color-accent">
{wave.period}s
</div>
<div className="text-[10px] text-fg-disabled"></div>
</div>
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono text-fg">{wave.direction}</div>
<div className="text-[10px] text-fg-disabled"></div>
</div>
</div>
{/* 파고 게이지 바 */}
<div className="flex items-center gap-2 mt-1.5">
<div className="flex-1 h-[5px] bg-bg-card rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${Math.min((wHgt / 5) * 100, 100)}%`,
background: waveColor(wHgt),
}}
/>
</div>
<span className="text-[11px] font-mono text-fg-disabled shrink-0">
{wHgt.toFixed(1)}/5m
</span>
</div>
</div>
{/* ── 수온/공기 ── */}
<div className="px-3 py-2 border-b border-stroke">
<div className="text-[11px] font-bold text-fg-disabled font-korean mb-2">
🌡 ·
</div>
<div className="grid grid-cols-3 gap-1">
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono text-color-accent">
{wTemp.toFixed(1)}°
</div>
<div className="text-[10px] text-fg-disabled"></div>
</div>
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono text-fg">
{temperature.feelsLike.toFixed(1)}°
</div>
<div className="text-[10px] text-fg-disabled"></div>
</div>
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-[14px] font-bold font-mono text-fg">{salinity.toFixed(1)}</div>
<div className="text-[10px] text-fg-disabled">(PSU)</div>
</div>
</div>
</div>
{/* ── 시간별 예보 ── */}
<div className="px-3 py-2 border-b border-stroke">
<div className="text-[11px] font-bold text-fg-disabled font-korean mb-2">
</div>
<div className="grid grid-cols-5 gap-1">
{forecast.map((f, i) => (
<div
key={i}
className="flex flex-col items-center py-2 px-1 bg-bg-base border border-stroke rounded"
>
<span className="text-[11px] text-fg-disabled mb-0.5">{f.hour}</span>
<span className="text-lg mb-0.5">{f.icon}</span>
<span className="text-[13px] font-bold text-fg">{f.temperature}°</span>
<span className="text-[11px] text-fg-disabled font-mono">{f.windSpeed}</span>
</div>
))}
</div>
</div>
{/* ── 천문/조석 ── */}
{astronomy && (
<div className="px-3 py-2 border-b border-stroke">
<div className="text-[11px] font-bold text-fg-disabled font-korean mb-2">
·
</div>
<div className="grid grid-cols-4 gap-1">
{[
{ icon: '🌅', label: '일출', value: astronomy.sunrise },
{ icon: '🌄', label: '일몰', value: astronomy.sunset },
{ icon: '🌙', label: '월출', value: astronomy.moonrise },
{ icon: '🌜', label: '월몰', value: astronomy.moonset },
].map((item, i) => (
<div key={i} className="text-center py-2 bg-bg-base border border-stroke rounded">
<div className="text-base mb-0.5">{item.icon}</div>
<div className="text-[10px] text-fg-disabled">{item.label}</div>
<div className="text-[13px] font-bold font-mono text-fg">{item.value}</div>
</div>
))}
</div>
<div className="flex items-center gap-2 mt-1.5 px-2 py-1 bg-bg-base border border-stroke rounded text-[11px]">
<span className="text-sm">🌓</span>
<span className="text-fg-disabled">{astronomy.moonPhase}</span>
<span className="ml-auto text-fg font-mono"> {astronomy.tidalRange}m</span>
</div>
</div>
)}
{/* ── 날씨 특보 ── */}
{alert && (
<div className="px-3 py-2">
<div className="text-[11px] font-bold text-fg-disabled font-korean mb-2">
🚨
</div>
<div
className="px-2.5 py-2 rounded border"
style={{ background: 'rgba(239,68,68,.06)', borderColor: 'rgba(239,68,68,.2)' }}
>
<div className="flex items-center gap-2 text-[11px]">
<span
className="px-1.5 py-px rounded text-[11px] font-bold"
style={{ background: 'rgba(239,68,68,.15)', color: 'var(--color-danger)' }}
>
</span>
<span className="text-fg font-korean">{alert}</span>
</div>
</div>
</div>
)}
</div>
</div>
);
}