2025-10-15 10:29:15 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import dynamic from "next/dynamic";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { RefreshCw } from "lucide-react";
|
2025-10-24 16:08:57 +09:00
|
|
|
|
import { getApiUrl } from "@/lib/utils/apiUrl";
|
2025-10-15 10:29:15 +09:00
|
|
|
|
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 });
|
2025-12-04 10:30:15 +09:00
|
|
|
|
const Polyline = dynamic(() => import("react-leaflet").then((mod) => mod.Polyline), { ssr: false });
|
2025-10-15 10:29:15 +09:00
|
|
|
|
|
|
|
|
|
|
// 브이월드 API 키
|
|
|
|
|
|
const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033";
|
|
|
|
|
|
|
|
|
|
|
|
interface Vehicle {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
driver: string;
|
|
|
|
|
|
lat: number;
|
|
|
|
|
|
lng: number;
|
2025-12-02 15:33:45 +09:00
|
|
|
|
status: "active" | "inactive" | "maintenance" | "warning" | "off";
|
2025-10-15 10:29:15 +09:00
|
|
|
|
speed: number;
|
|
|
|
|
|
destination: string;
|
2025-12-04 10:30:15 +09:00
|
|
|
|
userId?: string; // 이동경로 조회용
|
|
|
|
|
|
tripId?: string; // 현재 운행 ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 이동경로 좌표
|
|
|
|
|
|
interface RoutePoint {
|
|
|
|
|
|
lat: number;
|
|
|
|
|
|
lng: number;
|
|
|
|
|
|
recordedAt: string;
|
|
|
|
|
|
speed?: number;
|
2025-10-15 10:29:15 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface VehicleMapOnlyWidgetProps {
|
|
|
|
|
|
element?: any; // 대시보드 요소 (dataSource, chartConfig 포함)
|
|
|
|
|
|
refreshInterval?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000 }: VehicleMapOnlyWidgetProps) {
|
|
|
|
|
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
2025-12-04 10:30:15 +09:00
|
|
|
|
|
|
|
|
|
|
// 이동경로 상태
|
|
|
|
|
|
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
|
|
|
|
|
|
const [routePoints, setRoutePoints] = useState<RoutePoint[]>([]);
|
|
|
|
|
|
const [isRouteLoading, setIsRouteLoading] = useState(false);
|
2025-10-15 10:29:15 +09:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-10-24 16:08:57 +09:00
|
|
|
|
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
2025-10-15 10:29:15 +09:00
|
|
|
|
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 형식으로 변환
|
2025-12-02 15:33:45 +09:00
|
|
|
|
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 || "대기 중",
|
2025-12-04 10:30:15 +09:00
|
|
|
|
userId: row.user_id || row.userId || undefined,
|
|
|
|
|
|
tripId: row.trip_id || row.tripId || undefined,
|
2025-12-02 15:33:45 +09:00
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
// 유효한 위도/경도가 있는 차량만 필터링
|
|
|
|
|
|
.filter((v: Vehicle) => !isNaN(v.lat) && !isNaN(v.lng) && v.lat !== 0 && v.lng !== 0);
|
2025-10-15 10:29:15 +09:00
|
|
|
|
|
2025-12-02 15:33:45 +09:00
|
|
|
|
console.log("🗺️ [VehicleMapOnlyWidget] 유효한 차량 수:", vehiclesFromDB.length);
|
2025-10-15 10:29:15 +09:00
|
|
|
|
setVehicles(vehiclesFromDB);
|
|
|
|
|
|
setLastUpdate(new Date());
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("차량 데이터 로드 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-04 10:30:15 +09:00
|
|
|
|
// 이동경로 로드 함수
|
|
|
|
|
|
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([]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-15 10:29:15 +09:00
|
|
|
|
// useEffect는 항상 같은 순서로 호출되어야 함 (early return 전에 배치)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadVehicles();
|
|
|
|
|
|
const interval = setInterval(loadVehicles, refreshInterval);
|
|
|
|
|
|
return () => clearInterval(interval);
|
2025-10-22 13:40:15 +09:00
|
|
|
|
}, [
|
|
|
|
|
|
element?.dataSource?.query,
|
|
|
|
|
|
element?.chartConfig?.latitudeColumn,
|
|
|
|
|
|
element?.chartConfig?.longitudeColumn,
|
|
|
|
|
|
refreshInterval,
|
|
|
|
|
|
]);
|
2025-10-15 10:29:15 +09:00
|
|
|
|
|
|
|
|
|
|
// 쿼리 없으면 빈 지도만 표시 (안내 메시지 제거)
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="h-full w-full bg-gradient-to-br from-background to-primary/10 p-4">
|
2025-10-15 10:29:15 +09:00
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="mb-3 flex items-center justify-between">
|
|
|
|
|
|
<div>
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<h3 className="text-lg font-bold text-foreground">🗺️ 차량 위치 지도</h3>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
2025-10-15 10:29:15 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<Button variant="outline" size="sm" onClick={loadVehicles} disabled={isLoading} className="h-8 w-8 p-0">
|
|
|
|
|
|
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 지도 영역 - 브이월드 타일맵 */}
|
|
|
|
|
|
<div className="h-[calc(100%-60px)]">
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="relative z-0 h-full overflow-hidden rounded-lg border-2 border-border bg-background">
|
2025-10-15 10:29:15 +09:00
|
|
|
|
<MapContainer
|
2025-10-15 18:25:16 +09:00
|
|
|
|
key={`vehicle-map-${element.id}`}
|
2025-10-15 10:29:15 +09:00
|
|
|
|
center={[36.5, 127.5]}
|
|
|
|
|
|
zoom={7}
|
2025-10-15 18:25:16 +09:00
|
|
|
|
style={{ height: "100%", width: "100%", zIndex: 0 }}
|
2025-10-15 10:29:15 +09:00
|
|
|
|
zoomControl={true}
|
|
|
|
|
|
preferCanvas={true}
|
2025-10-15 18:25:16 +09:00
|
|
|
|
className="z-0"
|
2025-10-15 10:29:15 +09:00
|
|
|
|
>
|
2025-10-22 13:40:15 +09:00
|
|
|
|
{/* 브이월드 타일맵 (HTTPS, 캐싱 적용) */}
|
|
|
|
|
|
<TileLayer
|
|
|
|
|
|
url={`https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Base/{z}/{y}/{x}.png`}
|
|
|
|
|
|
attribution='© <a href="https://www.vworld.kr">VWorld (국토교통부)</a>'
|
|
|
|
|
|
maxZoom={19}
|
|
|
|
|
|
minZoom={7}
|
|
|
|
|
|
updateWhenIdle={true}
|
|
|
|
|
|
updateWhenZooming={false}
|
|
|
|
|
|
keepBuffer={2}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-04 10:30:15 +09:00
|
|
|
|
{/* 이동경로 Polyline */}
|
|
|
|
|
|
{routePoints.length > 1 && (
|
|
|
|
|
|
<Polyline
|
|
|
|
|
|
positions={routePoints.map((p) => [p.lat, p.lng] as [number, number])}
|
|
|
|
|
|
pathOptions={{
|
|
|
|
|
|
color: "#3b82f6",
|
|
|
|
|
|
weight: 4,
|
|
|
|
|
|
opacity: 0.8,
|
|
|
|
|
|
dashArray: "10, 5",
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-22 13:40:15 +09:00
|
|
|
|
{/* 차량 마커 */}
|
|
|
|
|
|
{vehicles.map((vehicle) => (
|
|
|
|
|
|
<React.Fragment key={vehicle.id}>
|
|
|
|
|
|
<Circle
|
|
|
|
|
|
center={[vehicle.lat, vehicle.lng]}
|
|
|
|
|
|
radius={150}
|
|
|
|
|
|
pathOptions={{
|
|
|
|
|
|
color: getStatusColor(vehicle.status),
|
|
|
|
|
|
fillColor: getStatusColor(vehicle.status),
|
|
|
|
|
|
fillOpacity: 0.3,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Marker position={[vehicle.lat, vehicle.lng]}>
|
|
|
|
|
|
<Popup>
|
|
|
|
|
|
<div className="text-xs">
|
|
|
|
|
|
<div className="mb-1 text-sm font-bold">{vehicle.name}</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>기사:</strong> {vehicle.driver}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>상태:</strong> {getStatusText(vehicle.status)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>속도:</strong> {vehicle.speed} km/h
|
2025-10-15 10:29:15 +09:00
|
|
|
|
</div>
|
2025-10-22 13:40:15 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<strong>목적지:</strong> {vehicle.destination}
|
|
|
|
|
|
</div>
|
2025-12-04 10:30:15 +09:00
|
|
|
|
{/* 이동경로 버튼 */}
|
|
|
|
|
|
{(vehicle.userId || vehicle.tripId) && (
|
|
|
|
|
|
<div className="mt-2 border-t pt-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => loadRoute(vehicle)}
|
|
|
|
|
|
disabled={isRouteLoading}
|
|
|
|
|
|
className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
{isRouteLoading && selectedVehicle?.id === vehicle.id
|
|
|
|
|
|
? "로딩 중..."
|
|
|
|
|
|
: "🛣️ 이동경로 보기"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-22 13:40:15 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</Popup>
|
|
|
|
|
|
</Marker>
|
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</MapContainer>
|
2025-10-15 10:29:15 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 지도 정보 */}
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="absolute top-2 right-2 z-[1000] rounded-lg bg-background/90 p-2 shadow-lg backdrop-blur-sm">
|
|
|
|
|
|
<div className="text-xs text-foreground">
|
2025-10-15 10:29:15 +09:00
|
|
|
|
<div className="mb-1 font-semibold">🗺️ 브이월드 (VWorld)</div>
|
|
|
|
|
|
<div className="text-xs">국토교통부 공식 지도</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 차량 수 표시 또는 설정 안내 */}
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="absolute bottom-2 left-2 z-[1000] rounded-lg bg-background/90 p-2 shadow-lg backdrop-blur-sm">
|
2025-10-15 10:29:15 +09:00
|
|
|
|
{vehicles.length > 0 ? (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="text-xs font-semibold text-foreground">총 {vehicles.length}대 모니터링 중</div>
|
2025-10-15 10:29:15 +09:00
|
|
|
|
) : (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="text-xs text-foreground">데이터를 연결하세요</div>
|
2025-10-15 10:29:15 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-12-04 10:30:15 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 이동경로 정보 표시 - 상단으로 이동하여 주석 처리 */}
|
|
|
|
|
|
{/* {selectedVehicle && routePoints.length > 0 && (
|
|
|
|
|
|
<div className="absolute bottom-2 right-2 z-[1000] rounded-lg bg-blue-500/90 p-2 shadow-lg backdrop-blur-sm">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<div className="text-xs text-white">
|
|
|
|
|
|
<div className="font-semibold">🛣️ {selectedVehicle.name} 이동경로</div>
|
|
|
|
|
|
<div>{routePoints.length}개 포인트</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={clearRoute}
|
|
|
|
|
|
className="rounded bg-white/20 px-2 py-1 text-xs text-white hover:bg-white/30"
|
|
|
|
|
|
>
|
|
|
|
|
|
숨기기
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)} */}
|
2025-10-15 10:29:15 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|