wing-ops/frontend/src/tabs/weather/components/WeatherRightPanel.tsx
jeonghyo.k e32c630da5 chore(weather): feature/cctv-hns-enhancements 머지 충돌 해결
WeatherRightPanel.tsx 충돌을 HEAD(feature/report) 기준으로 해결:
- WindCompass/ProgressBar/StatCard 재사용 컴포넌트 유지
- w-[380px] 너비 및 여유 패딩(px-5) 유지
- astronomy/alert props 기반 동적 데이터 유지

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 14:35:31 +09:00

278 lines
13 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;
}
/* ── Local Helpers (not exported) ─────────────────────────── */
function WindCompass({ degrees }: { degrees: number }) {
// center=28, radius=22
return (
<svg width="56" height="56" viewBox="0 0 56 56" className="shrink-0">
{/* arcs connecting N→E→S→W→N */}
<path d="M 28,6 A 22,22 0 0,1 50,28" fill="none" stroke="#1e2a42" strokeWidth="1" />
<path d="M 50,28 A 22,22 0 0,1 28,50" fill="none" stroke="#1e2a42" strokeWidth="1" />
<path d="M 28,50 A 22,22 0 0,1 6,28" fill="none" stroke="#1e2a42" strokeWidth="1" />
<path d="M 6,28 A 22,22 0 0,1 28,6" fill="none" stroke="#1e2a42" strokeWidth="1" />
{/* cardinal labels — same color as 풍향/기압 text (#edf0f7 = text-1) */}
<text x="28" y="10" textAnchor="middle" fontSize="8" fill="#edf0f7" fontWeight="600">N</text>
<text x="28" y="53" textAnchor="middle" fontSize="8" fill="#edf0f7" fontWeight="600">S</text>
<text x="50" y="31" textAnchor="middle" fontSize="8" fill="#edf0f7" fontWeight="600">E</text>
<text x="6" y="31" textAnchor="middle" fontSize="8" fill="#edf0f7" fontWeight="600">W</text>
{/* clock-hand needle */}
<g style={{ transform: `rotate(${degrees}deg)`, transformOrigin: '28px 28px' }}>
<line x1="28" y1="28" x2="28" y2="10" stroke="#eab308" strokeWidth="2" strokeLinecap="round" />
<circle cx="28" cy="10" r="2" fill="#eab308" />
</g>
{/* center dot */}
<circle cx="28" cy="28" r="2" fill="#8690a6" />
</svg>
);
}
function ProgressBar({ value, max, gradient, label }: { value: number; max: number; gradient: string; label: string }) {
const pct = Math.min(100, (value / max) * 100);
return (
<div className="mt-3 flex items-center gap-2">
<div className="h-1.5 flex-1 rounded-full bg-bg-2 overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${pct}%`, background: gradient }} />
</div>
<span className="text-[10px] text-text-3 shrink-0">{label}</span>
</div>
);
}
function StatCard({ value, label, valueClass = 'text-primary-cyan' }: { value: string; label: string; valueClass?: string }) {
return (
<div className="bg-bg-2 border border-border rounded-md p-2.5 flex flex-col items-center gap-0.5">
<span className={`text-sm font-bold font-mono ${valueClass}`}>{value}</span>
<span className="text-[10px] text-text-3">{label}</span>
</div>
);
}
/* ── Main Component ───────────────────────────────────────── */
export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
if (!weatherData) {
return (
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden w-[320px] shrink-0">
<div className="p-6 text-center">
<p className="text-text-3 text-[13px] font-korean"> </p>
</div>
</div>
);
}
const {
wind, wave, temperature, pressure, visibility,
salinity, astronomy, alert, forecast,
} = weatherData;
return (
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden w-[380px] shrink-0">
{/* ── Header ─────────────────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="flex items-center gap-2 mb-1">
<span className="text-primary-cyan text-sm font-semibold">📍 {weatherData.stationName}</span>
<span className="px-2 py-0.5 text-[10px] rounded bg-primary-cyan/20 text-primary-cyan font-semibold">
</span>
</div>
<p className="text-[11px] text-text-3 font-mono">
{weatherData.location.lat.toFixed(2)}°N, {weatherData.location.lon.toFixed(2)}°E · {weatherData.currentTime}
</p>
</div>
{/* ── Scrollable Content ─────────────────────────────── */}
<div className="flex-1 overflow-auto">
{/* ── Summary Cards ──────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="grid grid-cols-3 gap-2">
<div className="bg-bg-2 border border-border rounded-lg p-3 flex flex-col items-center">
<span className="text-[22px] font-bold text-primary-cyan font-mono leading-none">
{wind.speed.toFixed(1)}
</span>
<span className="text-[12px] text-text-3 mt-1"> (m/s)</span>
</div>
<div className="bg-bg-2 border border-border rounded-lg p-3 flex flex-col items-center">
<span className="text-[22px] font-bold text-primary-cyan font-mono leading-none">
{wave.height.toFixed(1)}
</span>
<span className="text-[12px] text-text-3 mt-1"> (m)</span>
</div>
<div className="bg-bg-2 border border-border rounded-lg p-3 flex flex-col items-center">
<span className="text-[22px] font-bold text-primary-cyan font-mono leading-none">
{temperature.current.toFixed(1)}
</span>
<span className="text-[12px] text-text-3 mt-1"> (°C)</span>
</div>
</div>
</div>
{/* ── 바람 현황 ──────────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="text-text-3 mb-3">🏳 </div>
<div className="flex gap-4 items-start">
<WindCompass degrees={wind.direction} />
<div className="flex-1 grid grid-cols-2 gap-x-4 gap-y-1.5 text-[13px]">
<span className="flex justify-between"><span className="text-text-3"></span><span className="text-text-1 font-semibold font-mono">{wind.directionLabel} {wind.direction}°</span></span>
<span className="flex justify-between"><span className="text-text-3"></span><span className="text-text-1 font-semibold font-mono">{pressure} hPa</span></span>
<span className="flex justify-between"><span className="text-text-3">1k </span><span className="text-text-1 font-semibold font-mono">{wind.speed_1k.toFixed(1)}</span></span>
<span className="flex justify-between"><span className="text-text-3">3k </span><span className="text-text-1 font-semibold font-mono">{wind.speed_3k.toFixed(1)}</span></span>
<span className="flex justify-between"><span className="text-text-3"></span><span className="text-text-1 font-semibold font-mono">{visibility} km</span></span>
</div>
</div>
<ProgressBar
value={wind.speed}
max={20}
gradient="linear-gradient(to right, #f97316, #eab308)"
label={`${wind.speed.toFixed(1)}/20`}
/>
</div>
{/* ── 파도 ───────────────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="text-[11px] text-text-3 mb-3">🌊 </div>
<div className="grid grid-cols-4 gap-1.5">
<StatCard value={`${wave.height.toFixed(1)}m`} label="유의파고" />
<StatCard value={`${wave.maxHeight.toFixed(1)}m`} label="최고파고" />
<StatCard value={`${wave.period}s`} label="주기" />
<StatCard value={wave.direction} label="파향" />
</div>
<ProgressBar
value={wave.height}
max={5}
gradient="linear-gradient(to right, #f97316, #6b7280)"
label={`${wave.height.toFixed(1)}/5m`}
/>
</div>
{/* ── 수온 · 공기 ────────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="text-[11px] text-text-3 mb-3">💧 · </div>
<div className="grid grid-cols-3 gap-1.5">
<StatCard value={`${temperature.current.toFixed(1)}°`} label="수온" />
<StatCard value={`${temperature.feelsLike.toFixed(1)}°`} label="기온" />
<StatCard value={`${salinity.toFixed(1)}`} label="염분(PSU)" />
</div>
</div>
{/* ── 시간별 예보 ────────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="text-[11px] text-text-3 mb-3">🕐 </div>
<div className="grid grid-cols-5 gap-1.5">
{forecast.map((fc, i) => (
<div
key={i}
className="bg-bg-2 border border-border rounded-md p-2 flex flex-col items-center gap-0.5"
>
<span className="text-[10px] text-text-3">{fc.hour}</span>
<span className="text-lg">{fc.icon}</span>
<span className="text-sm font-bold text-primary-cyan">{fc.temperature}°</span>
<span className="text-[10px] text-text-3">{fc.windSpeed}</span>
</div>
))}
</div>
</div>
{/* ── 천문 · 조석 ────────────────────────────────── */}
<div className="px-5 py-3 border-b border-border">
<div className="text-[11px] text-text-3 mb-3"> · </div>
{astronomy && (
<>
<div className="grid grid-cols-4 gap-1.5">
<div className="bg-bg-2 border border-border rounded-md p-2.5 flex flex-col items-center gap-1">
<span className="text-base">🌅</span>
<span className="text-[9px] text-text-3"></span>
<span className="text-xs font-bold text-text-1 font-mono">{astronomy.sunrise}</span>
</div>
<div className="bg-bg-2 border border-border rounded-md p-2.5 flex flex-col items-center gap-1">
<span className="text-base">🌇</span>
<span className="text-[9px] text-text-3"></span>
<span className="text-xs font-bold text-text-1 font-mono">{astronomy.sunset}</span>
</div>
<div className="bg-bg-2 border border-border rounded-md p-2.5 flex flex-col items-center gap-1">
<span className="text-base">🌙</span>
<span className="text-[9px] text-text-3"></span>
<span className="text-xs font-bold text-text-1 font-mono">{astronomy.moonrise}</span>
</div>
<div className="bg-bg-2 border border-border rounded-md p-2.5 flex flex-col items-center gap-1">
<span className="text-base">🌜</span>
<span className="text-[9px] text-text-3"></span>
<span className="text-xs font-bold text-text-1 font-mono">{astronomy.moonset}</span>
</div>
</div>
<div className="flex items-center justify-between mt-2 text-[11px] bg-bg-2 border border-border rounded-md p-2.5">
<div className="flex items-center gap-2">
<span>🌓</span>
<span className="text-text-3">{astronomy.moonPhase}</span>
</div>
<div className="text-text-3">
<span className="ml-2 text-text-1 font-semibold font-mono">{astronomy.tidalRange}m</span>
</div>
</div>
</>
)}
</div>
{/* ── 날씨 특보 ──────────────────────────────────── */}
{alert && (
<div className="px-5 py-3">
<div className="text-[11px] text-text-3 mb-3">🚨 </div>
<div className="p-3 bg-status-red/10 border border-status-red/30 rounded-md">
<div className="flex items-center gap-2">
<span className="px-1.5 py-0.5 text-[10px] font-bold rounded bg-status-red text-white"></span>
<span className="text-text-1 text-xs">{alert}</span>
</div>
</div>
</div>
)}
</div>
</div>
);
}