- {Object.entries(marker.info)
- .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase()))
- .map(([key, value]) => (
-
-
{key}: {String(value)}
+
+ {/* 마커 정보 */}
+
+
{marker.name}
+ {Object.entries(marker.info)
+ .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase()))
+ .map(([key, value]) => (
+
+ {key}: {String(value)}
+
+ ))}
+
+
+ {/* 날씨 정보 */}
+ {marker.weather && (
+
+
+ {getWeatherIcon(marker.weather.weatherMain)}
+ 현재 날씨
- ))}
+
{marker.weather.weatherDescription}
+
+
+ 온도
+ {marker.weather.temperature}°C
+
+
+ 체감온도
+ {marker.weather.feelsLike}°C
+
+
+ 습도
+ {marker.weather.humidity}%
+
+
+ 풍속
+ {marker.weather.windSpeed} m/s
+
+
+
+ )}
diff --git a/frontend/components/dashboard/widgets/WeatherMapWidget.tsx b/frontend/components/dashboard/widgets/WeatherMapWidget.tsx
new file mode 100644
index 00000000..035e7052
--- /dev/null
+++ b/frontend/components/dashboard/widgets/WeatherMapWidget.tsx
@@ -0,0 +1,211 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
+import { DashboardElement } from "@/components/admin/dashboard/types";
+import { getMultipleWeather, WeatherData } from "@/lib/api/openApi";
+import { Cloud, CloudRain, CloudSnow, Sun, Wind } from "lucide-react";
+import "leaflet/dist/leaflet.css";
+
+// Leaflet 아이콘 경로 설정 (엑박 방지)
+if (typeof window !== "undefined") {
+ const L = require("leaflet");
+ delete (L.Icon.Default.prototype as any)._getIconUrl;
+ L.Icon.Default.mergeOptions({
+ iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
+ iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
+ shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
+ });
+}
+
+// Leaflet 동적 import (SSR 방지)
+const MapContainer = dynamic(() => import("react-leaflet").then((mod) => mod.MapContainer), { ssr: false });
+const TileLayer = dynamic(() => import("react-leaflet").then((mod) => mod.TileLayer), { ssr: false });
+const Marker = dynamic(() => import("react-leaflet").then((mod) => mod.Marker), { ssr: false });
+const Popup = dynamic(() => import("react-leaflet").then((mod) => mod.Popup), { ssr: false });
+
+// 브이월드 API 키
+const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033";
+
+interface WeatherMapWidgetProps {
+ element: DashboardElement;
+ cities?: string[];
+}
+
+/**
+ * 날씨 아이콘 반환
+ */
+const getWeatherIcon = (weatherMain: string) => {
+ switch (weatherMain.toLowerCase()) {
+ case "clear":
+ return
;
+ case "rain":
+ return
;
+ case "snow":
+ return
;
+ case "clouds":
+ return
;
+ default:
+ return
;
+ }
+};
+
+/**
+ * 날씨 지도 위젯
+ * - 여러 도시의 날씨를 지도에 표시
+ * - 실시간 날씨 정보 (온도, 습도, 풍속 등)
+ * - Leaflet + 브이월드 지도 사용
+ */
+export default function WeatherMapWidget({ element, cities }: WeatherMapWidgetProps) {
+ const [weatherData, setWeatherData] = useState
([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // 기본 도시 목록 (사용자가 지정하지 않은 경우)
+ const defaultCities = [
+ "서울",
+ "부산",
+ "인천",
+ "대구",
+ "광주",
+ "대전",
+ "울산",
+ "세종",
+ "제주",
+ ];
+
+ const targetCities = cities || defaultCities;
+
+ useEffect(() => {
+ loadWeatherData();
+
+ // 자동 새로고침 (5분마다)
+ const interval = setInterval(() => {
+ loadWeatherData();
+ }, 300000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const loadWeatherData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const data = await getMultipleWeather(targetCities);
+
+ // 위도경도가 있는 데이터만 필터링
+ const validData = data.filter((item) => item.lat && item.lng);
+
+ setWeatherData(validData);
+ } catch (err: any) {
+ console.error("날씨 데이터 로드 실패:", err);
+ setError(err.message || "날씨 데이터를 불러오는데 실패했습니다.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading && weatherData.length === 0) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (weatherData.length === 0) {
+ return (
+
+ );
+ }
+
+ // 지도 중심 (대한민국 중심)
+ const center: [number, number] = [36.5, 127.5];
+
+ return (
+
+
+ {/* 브이월드 Base Map */}
+
+
+ {/* 날씨 마커 */}
+ {weatherData.map((weather, index) => {
+ if (!weather.lat || !weather.lng) return null;
+
+ return (
+
+
+
+ {/* 도시명 */}
+
+
{weather.city}
+ {getWeatherIcon(weather.weatherMain)}
+
+
+ {/* 날씨 설명 */}
+
{weather.weatherDescription}
+
+ {/* 날씨 정보 */}
+
+
+ 온도
+ {weather.temperature}°C
+
+
+ 체감온도
+ {weather.feelsLike}°C
+
+
+ 습도
+ {weather.humidity}%
+
+
+ 풍속
+ {weather.windSpeed} m/s
+
+
+
+ {/* 타임스탬프 */}
+
+ {new Date(weather.timestamp).toLocaleString("ko-KR", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
diff --git a/frontend/lib/api/openApi.ts b/frontend/lib/api/openApi.ts
index 89cbdd49..5cb90a5b 100644
--- a/frontend/lib/api/openApi.ts
+++ b/frontend/lib/api/openApi.ts
@@ -15,6 +15,8 @@ import { apiClient } from './client';
export interface WeatherData {
city: string;
country: string;
+ lat?: number;
+ lng?: number;
temperature: number;
feelsLike: number;
humidity: number;
@@ -79,6 +81,15 @@ export async function getWeather(
return response.data.data;
}
+/**
+ * 여러 도시의 날씨 정보 일괄 조회
+ * @param cities 도시명 배열
+ */
+export async function getMultipleWeather(cities: string[]): Promise {
+ const promises = cities.map(city => getWeather(city));
+ return Promise.all(promises);
+}
+
/**
* 환율 정보 조회
* @param base 기준 통화 (기본값: KRW)