release: 2026-03-19 (26건 커밋) #105
@ -33,12 +33,34 @@ 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-1 border-l border-border overflow-hidden w-[380px] shrink-0">
|
||||
<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-sm">지도에서 해양 지점을 클릭하세요</p>
|
||||
<p className="text-text-3 text-[11px] font-korean">지도에서 해양 지점을 클릭하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -48,225 +70,185 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
const sunsetTime = '17:58'
|
||||
const moonrise = '19:35'
|
||||
const moonset = '01:50'
|
||||
const moonPhase = '상현달 14일'
|
||||
const moonVisibility = '6.7 m'
|
||||
const windDir = windDirText(weatherData.wind.direction)
|
||||
const wSpd = Number(weatherData.wind.speed)
|
||||
const wHgt = Number(weatherData.wave.height)
|
||||
const wTemp = Number(weatherData.temperature.current)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden w-[380px] shrink-0">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-primary-cyan text-sm">📍 {weatherData.stationName}</span>
|
||||
<span className="px-2 py-0.5 text-xs rounded bg-primary-cyan/20 text-primary-cyan">
|
||||
기상예보관
|
||||
</span>
|
||||
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden w-[320px] shrink-0">
|
||||
{/* 헤더 */}
|
||||
<div className="px-4 py-3 border-b border-border bg-bg-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[11px] font-bold text-primary-cyan font-korean">📍 {weatherData.stationName}</span>
|
||||
<span className="px-1.5 py-px text-[8px] rounded bg-primary-cyan/15 text-primary-cyan font-bold">기상예보관</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-3">
|
||||
{weatherData.location.lat.toFixed(2)}°N, {weatherData.location.lon.toFixed(2)}°E · 현지시각{' '}
|
||||
{weatherData.currentTime}
|
||||
<p className="text-[9px] 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">
|
||||
{/* Wind Speed */}
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-text-3 text-xs">🌬️ 바람 현황</span>
|
||||
{/* 스크롤 콘텐츠 */}
|
||||
<div className="flex-1 overflow-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
|
||||
{/* ── 핵심 지표 3칸 카드 ── */}
|
||||
<div className="grid grid-cols-3 gap-1 px-3 py-2.5">
|
||||
<div className="text-center py-2.5 bg-bg-0 border border-border rounded-md">
|
||||
<div className="text-[18px] font-bold font-mono" style={{ color: windColor(wSpd) }}>{wSpd.toFixed(1)}</div>
|
||||
<div className="text-[8px] text-text-3 font-korean">풍속 (m/s)</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-bold text-text-1">{Number(weatherData.wind.speed).toFixed(1)}</span>
|
||||
<span className="text-sm text-text-3 ml-1">m/s</span>
|
||||
<span className="text-xs text-text-3 ml-2">NW 315°</span>
|
||||
<div className="text-center py-2.5 bg-bg-0 border border-border rounded-md">
|
||||
<div className="text-[18px] font-bold font-mono" style={{ color: waveColor(wHgt) }}>{wHgt.toFixed(1)}</div>
|
||||
<div className="text-[8px] text-text-3 font-korean">파고 (m)</div>
|
||||
</div>
|
||||
<div className="flex gap-4 mt-3 text-xs">
|
||||
<div>
|
||||
<span className="text-text-3">순간최고</span>
|
||||
<span className="text-text-1 font-semibold ml-2">
|
||||
1k:
|
||||
<span className="text-primary-cyan">{Number(weatherData.wind.speed_1k).toFixed(1)} m/s</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-3">평균</span>
|
||||
<span className="text-text-1 font-semibold ml-2">
|
||||
3k:
|
||||
<span className="text-status-yellow">{Number(weatherData.wind.speed_3k).toFixed(1)} m/s</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-text-3">
|
||||
<span>기압</span>
|
||||
<span className="text-text-1 font-semibold ml-2">
|
||||
{weatherData.pressure} hPa (Fresh)
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-text-3">
|
||||
<span>가시거리</span>
|
||||
<span className="text-text-1 font-semibold ml-2">{weatherData.visibility} km</span>
|
||||
<div className="text-center py-2.5 bg-bg-0 border border-border rounded-md">
|
||||
<div className="text-[18px] font-bold font-mono text-primary-cyan">{wTemp.toFixed(1)}</div>
|
||||
<div className="text-[8px] text-text-3 font-korean">수온 (°C)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wave Height */}
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-text-3 text-xs">🌊 파도</span>
|
||||
{/* ── 바람 상세 ── */}
|
||||
<div className="px-3 py-2 border-b border-border">
|
||||
<div className="text-[9px] font-bold text-text-3 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(--bd)" strokeWidth="1" />
|
||||
<circle cx="25" cy="25" r="16" fill="none" stroke="var(--bd)" 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(--t3)" fontSize="6" fontWeight="bold">{d}</text>
|
||||
})}
|
||||
{/* 풍향 화살표 */}
|
||||
<line
|
||||
x1="25" y1="25"
|
||||
x2={25 + 14 * Math.sin(weatherData.wind.direction * Math.PI / 180)}
|
||||
y2={25 - 14 * Math.cos(weatherData.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 text-[9px]">
|
||||
<div className="flex justify-between"><span className="text-text-3">풍향</span><span className="text-text-1 font-mono font-bold">{windDir} {weatherData.wind.direction}°</span></div>
|
||||
<div className="flex justify-between"><span className="text-text-3">기압</span><span className="text-text-1 font-mono">{weatherData.pressure} hPa</span></div>
|
||||
<div className="flex justify-between"><span className="text-text-3">1k 최고</span><span className="font-mono" style={{ color: windColor(weatherData.wind.speed_1k) }}>{Number(weatherData.wind.speed_1k).toFixed(1)}</span></div>
|
||||
<div className="flex justify-between"><span className="text-text-3">3k 평균</span><span className="font-mono" style={{ color: windColor(weatherData.wind.speed_3k) }}>{Number(weatherData.wind.speed_3k).toFixed(1)}</span></div>
|
||||
<div className="col-span-2 flex justify-between"><span className="text-text-3">가시거리</span><span className="text-text-1 font-mono">{weatherData.visibility} km</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-bold text-text-1">{Number(weatherData.wave.height).toFixed(1)}</span>
|
||||
<span className="text-sm text-text-3 ml-1">m</span>
|
||||
<span className="text-xs text-primary-cyan ml-2">주기:{weatherData.wave.period}초</span>
|
||||
{/* 풍속 게이지 바 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-[5px] bg-bg-3 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-[8px] font-mono text-text-3 shrink-0">{wSpd.toFixed(1)}/20</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<div className="text-text-3">치유파고</div>
|
||||
<div className="text-text-1 font-semibold">{Number(weatherData.wave.height).toFixed(1)} m</div>
|
||||
</div>
|
||||
|
||||
{/* ── 파도 상세 ── */}
|
||||
<div className="px-3 py-2 border-b border-border">
|
||||
<div className="text-[9px] font-bold text-text-3 font-korean mb-2">🌊 파도</div>
|
||||
<div className="grid grid-cols-4 gap-1 text-[9px]">
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono" style={{ color: waveColor(wHgt) }}>{wHgt.toFixed(1)}m</div>
|
||||
<div className="text-[7px] text-text-3">유의파고</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-text-3">최고파고</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
{(weatherData.wave.height * 1.6).toFixed(1)} m
|
||||
</div>
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono text-status-red">{(wHgt * 1.6).toFixed(1)}m</div>
|
||||
<div className="text-[7px] text-text-3">최고파고</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-text-3">파향</div>
|
||||
<div className="text-text-1 font-semibold">NW 128°</div>
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono text-primary-cyan">{weatherData.wave.period}s</div>
|
||||
<div className="text-[7px] text-text-3">주기</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-text-3">주기(초)</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
4 (Moderate)
|
||||
<span className="text-text-3 ml-1"></span>
|
||||
</div>
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono text-text-1">NW</div>
|
||||
<div className="text-[7px] text-text-3">파향</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 파고 게이지 바 */}
|
||||
<div className="flex items-center gap-2 mt-1.5">
|
||||
<div className="flex-1 h-[5px] bg-bg-3 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-[8px] font-mono text-text-3 shrink-0">{wHgt.toFixed(1)}/5m</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 수온/공기 ── */}
|
||||
<div className="px-3 py-2 border-b border-border">
|
||||
<div className="text-[9px] font-bold text-text-3 font-korean mb-2">🌡️ 수온 · 공기</div>
|
||||
<div className="grid grid-cols-3 gap-1 text-[9px]">
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono text-primary-cyan">{wTemp.toFixed(1)}°</div>
|
||||
<div className="text-[7px] text-text-3">수온</div>
|
||||
</div>
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono text-text-1">2.1°</div>
|
||||
<div className="text-[7px] text-text-3">기온</div>
|
||||
</div>
|
||||
<div className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="font-bold font-mono text-text-1">31.2</div>
|
||||
<div className="text-[7px] text-text-3">염분(PSU)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Temperature */}
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-text-3 text-xs">🌡️ 수온 • 공기</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-bold text-text-1">{Number(weatherData.temperature.current).toFixed(1)}</span>
|
||||
<span className="text-sm text-text-3">°C</span>
|
||||
<span className="text-xs text-text-3 ml-2">체감온도</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<div className="text-text-3">기온</div>
|
||||
<div className="text-text-1 font-semibold">2.1 °C</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-text-3">습도</div>
|
||||
<div className="text-text-1 font-semibold">31.2 PSU</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hourly Forecast */}
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-text-3 text-xs">⏰ 시간별 예보</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{weatherData.forecast.map((forecast, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col items-center p-2 bg-bg-2 border border-border rounded"
|
||||
>
|
||||
<span className="text-xs text-text-3 mb-1">{forecast.hour}</span>
|
||||
<span className="text-2xl mb-1">{forecast.icon}</span>
|
||||
<span className="text-sm font-semibold text-text-1 mb-1">
|
||||
{forecast.temperature}°
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-text-3">🌬️</span>
|
||||
<span className="text-xs text-text-2">{forecast.windSpeed}</span>
|
||||
</div>
|
||||
{/* ── 시간별 예보 ── */}
|
||||
<div className="px-3 py-2 border-b border-border">
|
||||
<div className="text-[9px] font-bold text-text-3 font-korean mb-2">⏰ 시간별 예보</div>
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
{weatherData.forecast.map((f, i) => (
|
||||
<div key={i} className="flex flex-col items-center py-1.5 px-1 bg-bg-0 border border-border rounded">
|
||||
<span className="text-[8px] text-text-3 mb-0.5">{f.hour}</span>
|
||||
<span className="text-base mb-0.5">{f.icon}</span>
|
||||
<span className="text-[10px] font-bold text-text-1">{f.temperature}°</span>
|
||||
<span className="text-[8px] text-text-3 font-mono">{f.windSpeed}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sun and Moon */}
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-text-3 text-xs">☀️ 천문 • 조석</span>
|
||||
{/* ── 천문/조석 ── */}
|
||||
<div className="px-3 py-2 border-b border-border">
|
||||
<div className="text-[9px] font-bold text-text-3 font-korean mb-2">☀️ 천문 · 조석</div>
|
||||
<div className="grid grid-cols-4 gap-1 text-[9px]">
|
||||
{[
|
||||
{ icon: '🌅', label: '일출', value: sunriseTime },
|
||||
{ icon: '🌄', label: '일몰', value: sunsetTime },
|
||||
{ icon: '🌙', label: '월출', value: moonrise },
|
||||
{ icon: '🌜', label: '월몰', value: moonset },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="text-center py-1.5 bg-bg-0 border border-border rounded">
|
||||
<div className="text-sm mb-0.5">{item.icon}</div>
|
||||
<div className="text-[7px] text-text-3">{item.label}</div>
|
||||
<div className="font-bold font-mono text-text-1">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🌅</span>
|
||||
<div className="text-xs">
|
||||
<div className="text-text-3">일출</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
{sunriseTime}
|
||||
<span className="text-text-3 ml-1">(8.6m)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🌄</span>
|
||||
<div className="text-xs">
|
||||
<div className="text-text-3">일몰</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
{sunsetTime}
|
||||
<span className="text-text-3 ml-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1.5 px-2 py-1 bg-bg-0 border border-border rounded text-[9px]">
|
||||
<span className="text-sm">🌓</span>
|
||||
<span className="text-text-3">상현달 14일</span>
|
||||
<span className="ml-auto text-text-1 font-mono">조차 6.7m</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🌙</span>
|
||||
<div className="text-xs">
|
||||
<div className="text-text-3">월출</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
{moonrise}
|
||||
<span className="text-text-3 ml-1">(8.8m)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🌜</span>
|
||||
<div className="text-xs">
|
||||
<div className="text-text-3">월몰</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
{moonset}
|
||||
<span className="text-text-3 ml-1">(1.8m)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🌓</span>
|
||||
<div className="text-xs">
|
||||
<div className="text-text-3">달</div>
|
||||
<div className="text-text-1 font-semibold">
|
||||
{moonPhase}
|
||||
<span className="text-text-3 ml-2">조차</span>
|
||||
<span className="text-text-1 ml-1">{moonVisibility}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* ── 날씨 특보 ── */}
|
||||
<div className="px-3 py-2">
|
||||
<div className="text-[9px] font-bold text-text-3 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-[9px]">
|
||||
<span className="px-1.5 py-px rounded text-[8px] font-bold" style={{ background: 'rgba(239,68,68,.15)', color: 'var(--red)' }}>주의</span>
|
||||
<span className="text-text-1 font-korean">풍랑주의보 예상 08:00~</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-text-3 text-xs">🚨 날씨 특보</span>
|
||||
</div>
|
||||
<div className="p-3 bg-status-red/10 border border-status-red/30 rounded">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-status-red text-xs">주의</span>
|
||||
<span className="text-text-1 text-xs">풍랑주의보 예상 08:00~</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user