"use client"; import React, { useState, useEffect } from "react"; import dynamic from "next/dynamic"; import { Button } from "@/components/ui/button"; import { RefreshCw } from "lucide-react"; import { getApiUrl } from "@/lib/utils/apiUrl"; 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 }); const Circle = dynamic(() => import("react-leaflet").then((mod) => mod.Circle), { ssr: false }); const Polyline = dynamic(() => import("react-leaflet").then((mod) => mod.Polyline), { ssr: false }); // 브이월드 API 키 const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033"; interface Vehicle { id: string; name: string; driver: string; lat: number; lng: number; status: "active" | "inactive" | "maintenance" | "warning" | "off"; speed: number; destination: string; userId?: string; // 이동경로 조회용 tripId?: string; // 현재 운행 ID } // 이동경로 좌표 interface RoutePoint { lat: number; lng: number; recordedAt: string; speed?: number; } interface VehicleMapOnlyWidgetProps { element?: any; // 대시보드 요소 (dataSource, chartConfig 포함) refreshInterval?: number; } export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000 }: VehicleMapOnlyWidgetProps) { const [vehicles, setVehicles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [lastUpdate, setLastUpdate] = useState(new Date()); // 이동경로 상태 const [selectedVehicle, setSelectedVehicle] = useState(null); const [routePoints, setRoutePoints] = useState([]); const [isRouteLoading, setIsRouteLoading] = useState(false); const loadVehicles = async () => { setIsLoading(true); // 설정된 쿼리가 없으면 로딩 중단 if (!element?.dataSource?.query) { setIsLoading(false); setVehicles([]); return; } // 설정된 컬럼 매핑 확인 if (!element?.chartConfig?.latitudeColumn || !element?.chartConfig?.longitudeColumn) { setIsLoading(false); setVehicles([]); return; } try { const response = await fetch(getApiUrl("/api/dashboards/execute-query"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`, }, body: JSON.stringify({ query: element.dataSource.query, }), }); if (response.ok) { const result = await response.json(); if (result.success && result.data.rows.length > 0) { // 설정된 컬럼 매핑 가져오기 const latCol = element.chartConfig.latitudeColumn; const lngCol = element.chartConfig.longitudeColumn; const labelCol = element.chartConfig.labelColumn || "name"; const statusCol = element.chartConfig.statusColumn || "status"; // DB 데이터를 Vehicle 형식으로 변환 console.log("🗺️ [VehicleMapOnlyWidget] 원본 데이터:", result.data.rows); console.log("🗺️ [VehicleMapOnlyWidget] 컬럼 매핑:", { latCol, lngCol, labelCol, statusCol }); const vehiclesFromDB: Vehicle[] = result.data.rows .map((row: any, index: number) => { const lat = parseFloat(row[latCol]); const lng = parseFloat(row[lngCol]); console.log(`🗺️ [VehicleMapOnlyWidget] 차량 ${index + 1}:`, { id: row.id || row.vehicle_number, latRaw: row[latCol], lngRaw: row[lngCol], latParsed: lat, lngParsed: lng, status: row[statusCol], }); return { id: row.id || row.vehicle_number || `V${index + 1}`, name: row[labelCol] || `차량 ${index + 1}`, driver: row.driver_name || row.driver || "미배정", lat, lng, status: row[statusCol] === "warning" ? "warning" : row[statusCol] === "active" ? "active" : row[statusCol] === "maintenance" ? "maintenance" : "inactive", speed: parseFloat(row.speed) || 0, destination: row.destination || "대기 중", userId: row.user_id || row.userId || undefined, tripId: row.trip_id || row.tripId || undefined, }; }) // 유효한 위도/경도가 있는 차량만 필터링 .filter((v: Vehicle) => !isNaN(v.lat) && !isNaN(v.lng) && v.lat !== 0 && v.lng !== 0); console.log("🗺️ [VehicleMapOnlyWidget] 유효한 차량 수:", vehiclesFromDB.length); setVehicles(vehiclesFromDB); setLastUpdate(new Date()); setIsLoading(false); return; } } } catch (error) { console.error("차량 데이터 로드 실패:", error); } setIsLoading(false); }; // 이동경로 로드 함수 const loadRoute = async (vehicle: Vehicle) => { if (!vehicle.userId && !vehicle.tripId) { console.log("🛣️ 이동경로 조회 불가: userId 또는 tripId 없음"); return; } setIsRouteLoading(true); setSelectedVehicle(vehicle); try { // 오늘 날짜 기준으로 최근 이동경로 조회 const today = new Date(); const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString(); // trip_id가 있으면 해당 운행만, 없으면 user_id로 오늘 전체 조회 let query = ""; if (vehicle.tripId) { query = `SELECT latitude, longitude, speed, recorded_at FROM vehicle_location_history WHERE trip_id = '${vehicle.tripId}' ORDER BY recorded_at ASC`; } else if (vehicle.userId) { query = `SELECT latitude, longitude, speed, recorded_at FROM vehicle_location_history WHERE user_id = '${vehicle.userId}' AND recorded_at >= '${startOfDay}' ORDER BY recorded_at ASC`; } console.log("🛣️ 이동경로 쿼리:", query); const response = await fetch(getApiUrl("/api/dashboards/execute-query"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`, }, body: JSON.stringify({ query }), }); if (response.ok) { const result = await response.json(); if (result.success && result.data.rows.length > 0) { const points: RoutePoint[] = result.data.rows.map((row: any) => ({ lat: parseFloat(row.latitude), lng: parseFloat(row.longitude), recordedAt: row.recorded_at, speed: row.speed ? parseFloat(row.speed) : undefined, })); console.log(`🛣️ 이동경로 ${points.length}개 포인트 로드 완료`); setRoutePoints(points); } else { console.log("🛣️ 이동경로 데이터 없음"); setRoutePoints([]); } } } catch (error) { console.error("이동경로 로드 실패:", error); setRoutePoints([]); } setIsRouteLoading(false); }; // 이동경로 숨기기 const clearRoute = () => { setSelectedVehicle(null); setRoutePoints([]); }; // useEffect는 항상 같은 순서로 호출되어야 함 (early return 전에 배치) useEffect(() => { loadVehicles(); const interval = setInterval(loadVehicles, refreshInterval); return () => clearInterval(interval); }, [ element?.dataSource?.query, element?.chartConfig?.latitudeColumn, element?.chartConfig?.longitudeColumn, refreshInterval, ]); // 쿼리 없으면 빈 지도만 표시 (안내 메시지 제거) const getStatusColor = (status: Vehicle["status"]) => { switch (status) { case "active": return "#22c55e"; // 운행 중 - 초록 case "inactive": return "#eab308"; // 대기 - 노랑 case "maintenance": return "#f97316"; // 정비 - 주황 case "warning": return "#ef4444"; // 고장 - 빨강 default: return "#6b7280"; // 기타 - 회색 } }; const getStatusText = (status: Vehicle["status"]) => { switch (status) { case "active": return "운행 중"; case "inactive": return "대기"; case "maintenance": return "정비"; case "warning": return "고장"; default: return "알 수 없음"; } }; return (
{/* 헤더 */}

🗺️ 차량 위치 지도

마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}

{/* 지도 영역 - 브이월드 타일맵 */}
{/* 브이월드 타일맵 (HTTPS, 캐싱 적용) */} {/* 이동경로 Polyline */} {routePoints.length > 1 && ( [p.lat, p.lng] as [number, number])} pathOptions={{ color: "#3b82f6", weight: 4, opacity: 0.8, dashArray: "10, 5", }} /> )} {/* 차량 마커 */} {vehicles.map((vehicle) => (
{vehicle.name}
기사: {vehicle.driver}
상태: {getStatusText(vehicle.status)}
속도: {vehicle.speed} km/h
목적지: {vehicle.destination}
{/* 이동경로 버튼 */} {(vehicle.userId || vehicle.tripId) && (
)}
))}
{/* 지도 정보 */}
🗺️ 브이월드 (VWorld)
국토교통부 공식 지도
{/* 차량 수 표시 또는 설정 안내 */}
{vehicles.length > 0 ? (
총 {vehicles.length}대 모니터링 중
) : (
데이터를 연결하세요
)}
{/* 이동경로 정보 표시 - 상단으로 이동하여 주석 처리 */} {/* {selectedVehicle && routePoints.length > 0 && (
🛣️ {selectedVehicle.name} 이동경로
{routePoints.length}개 포인트
)} */}
); }