"use client"; import React, { useState, useEffect } from "react"; import dynamic from "next/dynamic"; import { Button } from "@/components/ui/button"; import { RefreshCw, Truck, Navigation } 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 } ); const Circle = dynamic( () => import("react-leaflet").then((mod) => mod.Circle), { 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: "running" | "idle" | "maintenance" | "breakdown"; speed: number; destination: string; distance: number; fuel: number; avgSpeed: number; temperature?: number; isRefrigerated: boolean; } interface VehicleMapWidgetProps { refreshInterval?: number; } export default function VehicleMapWidget({ refreshInterval = 30000 }: VehicleMapWidgetProps) { const [vehicles, setVehicles] = useState([]); const [selectedVehicle, setSelectedVehicle] = useState(null); const [isLoading, setIsLoading] = useState(false); const [lastUpdate, setLastUpdate] = useState(new Date()); const loadVehicles = async () => { setIsLoading(true); const dummyVehicles: Vehicle[] = [ { id: "V001", name: "냉동차 1호", driver: "김기사", lat: 35.1796 + (Math.random() - 0.5) * 0.05, // 부산 lng: 129.0756 + (Math.random() - 0.5) * 0.05, status: "running", speed: 55 + Math.floor(Math.random() * 20), destination: "부산 → 울산", distance: 45 + Math.floor(Math.random() * 20), fuel: 85 + Math.floor(Math.random() * 15), avgSpeed: 62, temperature: -18 + Math.floor(Math.random() * 3), isRefrigerated: true, }, { id: "V002", name: "일반 화물차 2호", driver: "이기사", lat: 37.4563, // 인천 lng: 126.7052, status: "idle", speed: 0, destination: "대기 중", distance: 0, fuel: 5, avgSpeed: 0, isRefrigerated: false, }, { id: "V003", name: "냉장차 3호", driver: "박기사", lat: 36.3504 + (Math.random() - 0.5) * 0.05, // 대전 lng: 127.3845 + (Math.random() - 0.5) * 0.05, status: "running", speed: 40 + Math.floor(Math.random() * 15), destination: "대전 → 세종", distance: 22 + Math.floor(Math.random() * 10), fuel: 42 + Math.floor(Math.random() * 10), avgSpeed: 58, temperature: 2 + Math.floor(Math.random() * 4), isRefrigerated: true, }, { id: "V004", name: "일반 화물차 4호", driver: "최기사", lat: 35.8714, // 대구 lng: 128.6014, status: "maintenance", speed: 0, destination: "정비소", distance: 0, fuel: 0, avgSpeed: 0, isRefrigerated: false, }, { id: "V005", name: "냉동차 5호", driver: "정기사", lat: 33.4996 + (Math.random() - 0.5) * 0.05, // 제주 lng: 126.5312 + (Math.random() - 0.5) * 0.05, status: "running", speed: 45 + Math.floor(Math.random() * 15), destination: "제주 → 서귀포", distance: 28 + Math.floor(Math.random() * 10), fuel: 52 + Math.floor(Math.random() * 10), avgSpeed: 54, temperature: -20 + Math.floor(Math.random() * 2), isRefrigerated: true, }, { id: "V006", name: "일반 화물차 6호", driver: "강기사", lat: 35.1595, // 광주 lng: 126.8526, status: "breakdown", speed: 0, destination: "고장 (견인 대기)", distance: 65, fuel: 18, avgSpeed: 0, isRefrigerated: false, }, { id: "V007", name: "냉장차 7호", driver: "윤기사", lat: 37.5665 + (Math.random() - 0.5) * 0.05, // 서울 lng: 126.9780 + (Math.random() - 0.5) * 0.05, status: "running", speed: 60 + Math.floor(Math.random() * 15), destination: "서울 → 수원", distance: 35 + Math.floor(Math.random() * 10), fuel: 68 + Math.floor(Math.random() * 10), avgSpeed: 65, temperature: 3 + Math.floor(Math.random() * 3), isRefrigerated: true, }, { id: "V008", name: "일반 화물차 8호", driver: "한기사", lat: 37.8813 + (Math.random() - 0.5) * 0.05, // 춘천 lng: 127.7300 + (Math.random() - 0.5) * 0.05, status: "running", speed: 50 + Math.floor(Math.random() * 15), destination: "춘천 → 강릉", distance: 95 + Math.floor(Math.random() * 20), fuel: 75 + Math.floor(Math.random() * 15), avgSpeed: 58, isRefrigerated: false, }, ]; setTimeout(() => { setVehicles(dummyVehicles); setLastUpdate(new Date()); setIsLoading(false); }, 500); }; useEffect(() => { loadVehicles(); const interval = setInterval(loadVehicles, refreshInterval); return () => clearInterval(interval); }, [refreshInterval]); const getStatusColor = (status: Vehicle["status"]) => { switch (status) { case "running": return "#22c55e"; case "idle": return "#eab308"; case "maintenance": return "#f97316"; case "breakdown": return "#ef4444"; default: return "#6b7280"; } }; const getStatusText = (status: Vehicle["status"]) => { switch (status) { case "running": return "운행 중"; case "idle": return "대기"; case "maintenance": return "정비"; case "breakdown": return "고장"; default: return "알 수 없음"; } }; const statusStats = { running: vehicles.filter((v) => v.status === "running").length, idle: vehicles.filter((v) => v.status === "idle").length, maintenance: vehicles.filter((v) => v.status === "maintenance").length, breakdown: vehicles.filter((v) => v.status === "breakdown").length, }; return (
{/* 헤더 */}

🚚 실시간 차량 위치

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

{/* 차량 상태 요약 */}
운행 중
{statusStats.running}대
대기
{statusStats.idle}대
정비
{statusStats.maintenance}대
고장
{statusStats.breakdown}대
{/* 지도 영역 - 브이월드 타일맵 */}
{typeof window !== "undefined" && ( {/* 브이월드 타일맵 (HTTPS, 캐싱 적용) */} {/* 차량 마커 */} {vehicles.map((vehicle) => ( setSelectedVehicle(vehicle), }} >
{vehicle.name}
기사: {vehicle.driver}
상태: {getStatusText(vehicle.status)}
속도: {vehicle.speed} km/h
거리: {vehicle.distance} km
연료: {vehicle.fuel} L
{vehicle.isRefrigerated && vehicle.temperature !== undefined && (
온도: {vehicle.temperature}°C
)}
))}
)} {/* 지도 정보 */}
🗺️ 브이월드 (VWorld)
국토교통부 공식 지도
{/* 차량 수 표시 */}
총 {vehicles.length}대 모니터링 중
{/* 우측 사이드 패널 */}
{/* 차량 목록 */}

차량 목록 ({vehicles.length}대)

{vehicles.length === 0 ? (
차량이 없습니다
) : (
{vehicles.map((vehicle) => (
setSelectedVehicle(vehicle)} className={`cursor-pointer rounded-lg border p-2 transition-all hover:shadow-sm ${ selectedVehicle?.id === vehicle.id ? "border-gray-900 bg-gray-50 ring-1 ring-gray-900" : "border-gray-200 bg-white hover:border-gray-300" }`} >
{vehicle.name} {getStatusText(vehicle.status)}
{vehicle.destination}
))}
)}
{/* 선택된 차량 상세 정보 */} {selectedVehicle ? (
{/* 헤더 */}

{selectedVehicle.name}

{getStatusText(selectedVehicle.status)} {selectedVehicle.id}
{/* 기사 정보 */}
👤 기사 정보
이름 {selectedVehicle.driver}
GPS 좌표 {selectedVehicle.lat.toFixed(4)}, {selectedVehicle.lng.toFixed(4)}
{/* 운행 정보 */}
📍 운행 정보
목적지 {selectedVehicle.destination}
{/* 실시간 데이터 */}
📊 실시간 데이터
현재 속도
{selectedVehicle.speed}
km/h
평균 속도
{selectedVehicle.avgSpeed}
km/h
운행 거리
{selectedVehicle.distance}
km
소모 연료
{selectedVehicle.fuel}
L
{/* 냉동/냉장 상태 */} {selectedVehicle.isRefrigerated && selectedVehicle.temperature !== undefined && (
❄️ 냉동/냉장 상태
현재 온도 {selectedVehicle.temperature}°C
타입 {selectedVehicle.temperature < -10 ? "냉동" : "냉장"}
적정 범위 {selectedVehicle.temperature < -10 ? "-18°C ~ -15°C" : "0°C ~ 5°C"}
상태 {Math.abs(selectedVehicle.temperature - (selectedVehicle.temperature < -10 ? -18 : 2)) < 5 ? "✓ 정상" : "⚠ 주의"}
)}
) : (

차량을 선택하면

상세 정보가 표시됩니다

)}
); }