import { useState, useMemo, useCallback } from 'react' import { Map, Marker, useControl } from '@vis.gl/react-maplibre' import { MapboxOverlay } from '@deck.gl/mapbox' import type { Layer } from '@deck.gl/core' import type { MapLayerMouseEvent } from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle' import { S57EncOverlay } from '@common/components/map/S57EncOverlay' import { useMapStore } from '@common/store/mapStore' import { WeatherRightPanel } from './WeatherRightPanel' import { WeatherMapOverlay, useWeatherDeckLayers } from './WeatherMapOverlay' // import { OceanForecastOverlay } from './OceanForecastOverlay' // import { useOceanCurrentLayers } from './OceanCurrentLayer' import { useWaterTemperatureLayers } from './WaterTemperatureLayer' import { WindParticleLayer } from './WindParticleLayer' import { OceanCurrentParticleLayer } from './OceanCurrentParticleLayer' import { useWeatherData } from '../hooks/useWeatherData' // import { useOceanForecast } from '../hooks/useOceanForecast' import { WeatherMapControls } from './WeatherMapControls' import { degreesToCardinal } from '../services/weatherUtils' type TimeOffset = '0' | '3' | '6' | '9' interface WeatherStation { id: string name: string location: { lat: number; lon: number } wind: { speed: number direction: number speed_1k: number speed_3k: number } wave: { height: number period: number } temperature: { current: number feelsLike: number } pressure: number visibility: number salinity?: number } interface WeatherForecast { time: string hour: string icon: string temperature: number windSpeed: number } // Base weather station locations const BASE_STATIONS = [ { id: 'incheon', name: '인천', location: { lat: 37.45, lon: 126.43 } }, { id: 'ulsan', name: '울산', location: { lat: 35.52, lon: 129.38 } }, { id: 'yeosu', name: '여수', location: { lat: 34.74, lon: 127.75 } }, { id: 'jeju', name: '제주', location: { lat: 33.51, lon: 126.53 } }, { id: 'pohang', name: '포항', location: { lat: 36.03, lon: 129.38 } }, { id: 'mokpo', name: '목포', location: { lat: 34.78, lon: 126.38 } }, { id: 'gunsan', name: '군산', location: { lat: 35.97, lon: 126.7 } }, { id: 'sokcho', name: '속초', location: { lat: 38.21, lon: 128.59 } }, { id: 'tongyeong', name: '통영', location: { lat: 34.83, lon: 128.43 } }, { id: 'donghae', name: '동해', location: { lat: 37.52, lon: 129.14 } }, ] // Generate forecast data based on time offset const generateForecast = (timeOffset: TimeOffset): WeatherForecast[] => { const baseHour = parseInt(timeOffset) const forecasts: WeatherForecast[] = [] const icons = ['☀️', '⛅', '☁️', '🌦️', '🌧️'] for (let i = 0; i < 5; i++) { const hour = baseHour + i * 3 forecasts.push({ time: `+${hour}시`, hour: `${hour}시`, icon: icons[i % icons.length], temperature: Math.floor(Math.random() * 5) + 5, windSpeed: Math.floor(Math.random() * 5) + 6, }) } return forecasts } // 한국 해역 중심 좌표 (한반도 중앙) const WEATHER_MAP_CENTER: [number, number] = [127.8, 36.5] // [lng, lat] const WEATHER_MAP_ZOOM = 7 // deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록) // eslint-disable-next-line @typescript-eslint/no-explicit-any function DeckGLOverlay({ layers }: { layers: Layer[] }) { const overlay = useControl(() => new MapboxOverlay({ interleaved: true })) overlay.setProps({ layers }) return null } /** * WeatherMapInner — Map 컴포넌트 내부 (useMap / useControl 사용 가능 영역) */ interface WeatherMapInnerProps { weatherStations: WeatherStation[] enabledLayers: Set selectedStationId: string | null onStationClick: (station: WeatherStation) => void mapCenter: [number, number] mapZoom: number clickedLocation: { lat: number; lon: number } | null } function WeatherMapInner({ weatherStations, enabledLayers, selectedStationId, onStationClick, mapCenter, mapZoom, clickedLocation, }: WeatherMapInnerProps) { // deck.gl layers 조합 const weatherDeckLayers = useWeatherDeckLayers( weatherStations, enabledLayers, selectedStationId, onStationClick ) // const oceanCurrentLayers = useOceanCurrentLayers({ // visible: enabledLayers.has('oceanCurrent'), // opacity: 0.7, // }) const waterTempLayers = useWaterTemperatureLayers({ visible: enabledLayers.has('waterTemperature'), opacity: 0.5, }) const deckLayers = useMemo( () => [...waterTempLayers, ...weatherDeckLayers], [waterTempLayers, weatherDeckLayers] ) return ( <> {/* deck.gl 오버레이 */} {/* 해황예보도 — 임시 비활성화 */} {/* 해류 흐름 파티클 애니메이션 (Canvas 직접 조작) */} {/* 기상 관측소 HTML 오버레이 (풍향 화살표 + 라벨) */} {/* 바람 파티클 애니메이션 (Canvas 직접 조작) */} {/* 클릭 위치 마커 */} {clickedLocation && (
{/* 펄스 링 */}
{/* 핀 꼬리 */}
{/* 좌표 라벨 */}
{clickedLocation.lat.toFixed(3)}°N {clickedLocation.lon.toFixed(3)}°E
)} {/* 줌 컨트롤 */} ) } export function WeatherView() { const { weatherStations, loading, error, lastUpdate } = useWeatherData(BASE_STATIONS) const currentMapStyle = useBaseMapStyle() const mapToggles = useMapStore((s) => s.mapToggles) // const { // selectedForecast, // availableTimes, // loading: oceanLoading, // error: oceanError, // selectForecast, // } = useOceanForecast('KOREA') const [timeOffset, setTimeOffset] = useState('0') const [selectedStationRaw, setSelectedStation] = useState(null) const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lon: number } | null>( null ) const [enabledLayers, setEnabledLayers] = useState>(new Set(['wind'])) // const [oceanForecastOpacity, setOceanForecastOpacity] = useState(0.6) // 첫 관측소 자동 선택 (파생 값) const selectedStation = selectedStationRaw ?? weatherStations[0] ?? null const handleStationClick = useCallback( (station: WeatherStation) => { setSelectedStation(station) setSelectedLocation(null) }, [] ) const handleMapClick = useCallback( (e: MapLayerMouseEvent) => { const { lat, lng } = e.lngLat if (weatherStations.length === 0) return // 가장 가까운 관측소 선택 const nearestStation = weatherStations.reduce((nearest, station) => { const distance = Math.sqrt( Math.pow(station.location.lat - lat, 2) + Math.pow(station.location.lon - lng, 2) ) const nearestDistance = Math.sqrt( Math.pow(nearest.location.lat - lat, 2) + Math.pow(nearest.location.lon - lng, 2) ) return distance < nearestDistance ? station : nearest }, weatherStations[0]) setSelectedStation(nearestStation) setSelectedLocation({ lat, lon: lng }) }, [weatherStations] ) const toggleLayer = useCallback((layer: string) => { setEnabledLayers((prev) => { const next = new Set(prev) if (next.has(layer)) { next.delete(layer) } else { next.add(layer) } return next }) }, []) const weatherData = selectedStation ? { stationName: selectedLocation ? `해양 지점 (${selectedLocation.lat.toFixed(2)}°N, ${selectedLocation.lon.toFixed(2)}°E)` : selectedStation.name, location: selectedLocation || selectedStation.location, currentTime: new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', }), wind: { ...selectedStation.wind, directionLabel: degreesToCardinal(selectedStation.wind.direction), }, wave: { ...selectedStation.wave, maxHeight: Number((selectedStation.wave.height * 1.6).toFixed(1)), direction: degreesToCardinal(selectedStation.wind.direction + 45), }, temperature: selectedStation.temperature, pressure: selectedStation.pressure, visibility: selectedStation.visibility, salinity: selectedStation.salinity ?? 31.2, astronomy: { sunrise: '07:12', sunset: '17:58', moonrise: '19:35', moonset: '01:50', moonPhase: '상현달 14일', tidalRange: 6.7, }, alert: '풍랑주의보 예상 08:00~', forecast: generateForecast(timeOffset), } : null return (
{/* Main Map Area */}
{/* Tab Navigation */}
{(['0', '3', '6', '9'] as TimeOffset[]).map((offset) => ( ))}
{lastUpdate ? `마지막 업데이트: ${lastUpdate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', })}` : '데이터 로딩 중...'} {loading && (
)} {error && ⚠️ {error}}
{/* Map */}
{/* 레이어 컨트롤 */}
기상 레이어
{/* 범례 */}
기상 범례
{/* 바람 */}
바람 (m/s)
35710131620+
{/* 해류 */}
해류 (m/s)
0.20.40.60.6+
{/* 파고 */}
파고 (m)
<1.5 낮음
~2.5
>2.5
💡 지도 클릭 → 기상 예보 확인
{/* Right Panel */}
) }