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>
278 lines
13 KiB
TypeScript
Executable File
278 lines
13 KiB
TypeScript
Executable File
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>
|
||
);
|
||
}
|