feat(weather): 기상 정보 기상 레이어 업데이트 #78

병합
dnlee feature/add-weather-alarm 에서 develop 로 10 commits 를 머지했습니다 2026-03-11 11:14:25 +09:00
5개의 변경된 파일88개의 추가작업 그리고 42개의 파일을 삭제
Showing only changes of commit 174f4eb9f2 - Show all commits

파일 보기

@ -558,7 +558,6 @@
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@ -1992,7 +1991,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz",
"integrity": "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.11.0",
"pg-pool": "^3.12.0",

파일 보기

@ -0,0 +1,48 @@
import { useMap } from '@vis.gl/react-maplibre'
interface WeatherMapControlsProps {
center: [number, number]
zoom: number
}
export function WeatherMapControls({ center, zoom }: WeatherMapControlsProps) {
const { current: map } = useMap()
const buttons = [
{
label: '+',
tooltip: '확대',
onClick: () => map?.zoomIn(),
},
{
label: '',
tooltip: '축소',
onClick: () => map?.zoomOut(),
},
{
label: '🎯',
tooltip: '한국 해역 초기화',
onClick: () => map?.flyTo({ center, zoom, duration: 1000 }),
},
]
return (
<div className="absolute top-4 right-4 z-10">
<div className="flex flex-col gap-2">
{buttons.map(({ label, tooltip, onClick }) => (
<div key={tooltip} className="relative group">
<button
onClick={onClick}
className="w-[38px] h-[38px] bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-sm text-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all text-base"
>
{label}
</button>
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-2 px-2 py-1 text-xs bg-bg-0 text-text-1 border border-border rounded whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-20">
{tooltip}
</div>
</div>
))}
</div>
</div>
)
}

파일 보기

@ -1,5 +1,5 @@
import { useState, useMemo, useCallback } from 'react'
import { Map, useControl, useMap } from '@vis.gl/react-maplibre'
import { Map, Marker, useControl } from '@vis.gl/react-maplibre'
import { MapboxOverlay } from '@deck.gl/mapbox'
import type { Layer } from '@deck.gl/core'
import type { StyleSpecification, MapLayerMouseEvent } from 'maplibre-gl'
@ -12,6 +12,7 @@ import { useWaterTemperatureLayers } from './WaterTemperatureLayer'
import { WindParticleLayer } from './WindParticleLayer'
import { useWeatherData } from '../hooks/useWeatherData'
import { useOceanForecast } from '../hooks/useOceanForecast'
import { WeatherMapControls } from './WeatherMapControls'
type TimeOffset = '0' | '3' | '6' | '9'
@ -117,38 +118,6 @@ function DeckGLOverlay({ layers }: { layers: Layer[] }) {
return null
}
// 줌 컨트롤
function WeatherMapControls() {
const { current: map } = useMap()
return (
<div className="absolute top-4 right-4 z-10">
<div className="flex flex-col gap-2">
<button
onClick={() => map?.zoomIn()}
className="w-[38px] h-[38px] bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-sm text-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all text-base"
>
+
</button>
<button
onClick={() => map?.zoomOut()}
className="w-[38px] h-[38px] bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-sm text-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all text-base"
>
</button>
<button
onClick={() =>
map?.flyTo({ center: WEATHER_MAP_CENTER, zoom: WEATHER_MAP_ZOOM, duration: 1000 })
}
className="w-[38px] h-[38px] bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-sm text-text-2 flex items-center justify-center hover:text-text-1 transition-all text-sm"
>
&#x1F3AF;
</button>
</div>
</div>
)
}
/**
* WeatherMapInner Map (useMap / useControl )
*/
@ -159,6 +128,9 @@ interface WeatherMapInnerProps {
oceanForecastOpacity: number
selectedForecast: ReturnType<typeof useOceanForecast>['selectedForecast']
onStationClick: (station: WeatherStation) => void
mapCenter: [number, number]
mapZoom: number
clickedLocation: { lat: number; lon: number } | null
}
function WeatherMapInner({
@ -168,6 +140,9 @@ function WeatherMapInner({
oceanForecastOpacity,
selectedForecast,
onStationClick,
mapCenter,
mapZoom,
clickedLocation,
}: WeatherMapInnerProps) {
// deck.gl layers 조합
const weatherDeckLayers = useWeatherDeckLayers(
@ -216,8 +191,31 @@ function WeatherMapInner({
stations={weatherStations}
/>
{/* 클릭 위치 마커 */}
{clickedLocation && (
<Marker
longitude={clickedLocation.lon}
latitude={clickedLocation.lat}
anchor="bottom"
>
<div className="flex flex-col items-center pointer-events-none">
{/* 펄스 링 */}
<div className="relative flex items-center justify-center">
<div className="absolute w-8 h-8 rounded-full border-2 border-primary-cyan animate-ping opacity-60" />
<div className="w-4 h-4 rounded-full bg-primary-cyan border-2 border-white shadow-lg" />
</div>
{/* 핀 꼬리 */}
<div className="w-px h-3 bg-primary-cyan" />
{/* 좌표 라벨 */}
<div className="px-2 py-1 bg-bg-0/90 border border-primary-cyan rounded text-[10px] text-primary-cyan whitespace-nowrap backdrop-blur-sm">
{clickedLocation.lat.toFixed(3)}°N&nbsp;{clickedLocation.lon.toFixed(3)}°E
</div>
</div>
</Marker>
)}
{/* 줌 컨트롤 */}
<WeatherMapControls />
<WeatherMapControls center={mapCenter} zoom={mapZoom} />
</>
)
}
@ -225,6 +223,8 @@ function WeatherMapInner({
export function WeatherView() {
const { weatherStations, loading, error, lastUpdate } = useWeatherData(BASE_STATIONS)
console.log(weatherStations,'날씨');
const {
selectedForecast,
availableTimes,
@ -343,12 +343,6 @@ export function WeatherView() {
</div>
<div className="flex-1" />
<div className="px-6">
<button className="px-4 py-2 text-xs font-semibold rounded bg-status-red text-white hover:opacity-90 transition-opacity">
🚨
</button>
</div>
</div>
{/* Map */}
@ -371,6 +365,9 @@ export function WeatherView() {
oceanForecastOpacity={oceanForecastOpacity}
selectedForecast={selectedForecast}
onStationClick={handleStationClick}
mapCenter={WEATHER_MAP_CENTER}
mapZoom={WEATHER_MAP_ZOOM}
clickedLocation={selectedLocation}
/>
</Map>

파일 보기

@ -87,6 +87,7 @@ export function useWeatherData(stations: WeatherStation[]) {
}
const obs = await getRecentObservation(obsCode)
if (obs) {
const r = (n: number) => Math.round(n * 10) / 10

파일 보기

@ -157,6 +157,8 @@ export async function getRecentObservation(obsCode: string): Promise<RecentObser
})
const response = await fetch(`${RECENT_OBS_URL}?${params}`)
console.log(response,'리스폰스');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}