From c64c94c07b5a2b6a9a3dbb00a72a20b5fc7a4b3c Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 10 Dec 2025 13:48:57 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B5=9C=EA=B7=BC=EC=9D=B4=EB=8F=99=ED=95=9C?= =?UTF-8?q?=20=EB=82=B4=EC=97=AD=EB=93=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/widgets/MapTestWidgetV2.tsx | 222 +++++++++++++++++- 1 file changed, 219 insertions(+), 3 deletions(-) diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index 48545281..c1553b38 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -103,6 +103,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const [routeLoading, setRouteLoading] = useState(false); const [routeDate, setRouteDate] = useState(new Date().toISOString().split("T")[0]); // YYYY-MM-DD 형식 + // 공차/운행 정보 상태 + const [tripInfo, setTripInfo] = useState>({}); + const [tripInfoLoading, setTripInfoLoading] = useState(null); + + // Popup 열림 상태 (자동 새로고침 일시 중지용) + const [isPopupOpen, setIsPopupOpen] = useState(false); + // 지역 필터 상태 const [selectedRegion, setSelectedRegion] = useState("all"); @@ -187,6 +194,51 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { setRoutePoints([]); }, []); + // 공차/운행 정보 로드 함수 + const loadTripInfo = useCallback(async (identifier: string) => { + if (!identifier || tripInfo[identifier]) { + return; // 이미 로드됨 + } + + setTripInfoLoading(identifier); + + try { + // user_id 또는 vehicle_number로 조회 + const query = `SELECT + id, vehicle_number, user_id, + last_trip_start, last_trip_end, last_trip_distance, last_trip_time, + last_empty_start, last_empty_end, last_empty_distance, last_empty_time, + departure, arrival, status + FROM vehicles + WHERE user_id = '${identifier}' + OR vehicle_number = '${identifier}' + LIMIT 1`; + + 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) { + setTripInfo((prev) => ({ + ...prev, + [identifier]: result.data.rows[0], + })); + } + } + } catch (err) { + console.error("공차/운행 정보 로드 실패:", err); + } + + setTripInfoLoading(null); + }, [tripInfo]); + // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { if (!dataSources || dataSources.length === 0) { @@ -1135,14 +1187,17 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } const intervalId = setInterval(() => { - loadMultipleDataSources(); + // Popup이 열려있으면 자동 새로고침 건너뛰기 + if (!isPopupOpen) { + loadMultipleDataSources(); + } }, refreshInterval * 1000); return () => { clearInterval(intervalId); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataSources, element?.chartConfig?.refreshInterval]); + }, [dataSources, element?.chartConfig?.refreshInterval, isPopupOpen]); // 타일맵 URL (VWorld 한국 지도) const tileMapUrl = `https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Base/{z}/{y}/{x}.png`; @@ -1390,6 +1445,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { fillOpacity: 0.3, weight: 2, }} + eventHandlers={{ + popupopen: () => setIsPopupOpen(true), + popupclose: () => setIsPopupOpen(false), + }} >
@@ -1621,7 +1680,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } return ( - + setIsPopupOpen(true), + popupclose: () => setIsPopupOpen(false), + }} + >
{/* 데이터 소스명만 표시 */} @@ -1732,6 +1799,155 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } })()} + {/* 공차/운행 정보 (동적 로딩) */} + {(() => { + try { + const parsed = JSON.parse(marker.description || "{}"); + + // 식별자 찾기 (user_id 또는 vehicle_number) + const identifier = parsed.user_id || parsed.userId || parsed.vehicle_number || + parsed.vehicleNumber || parsed.plate_no || parsed.plateNo || + parsed.car_number || parsed.carNumber || marker.name; + + if (!identifier) return null; + + // 동적으로 로드된 정보 또는 marker.description에서 가져온 정보 사용 + const info = tripInfo[identifier] || parsed; + + // 공차 정보가 있는지 확인 + const hasEmptyTripInfo = info.last_empty_start || info.last_empty_end || + info.last_empty_distance || info.last_empty_time; + // 운행 정보가 있는지 확인 + const hasTripInfo = info.last_trip_start || info.last_trip_end || + info.last_trip_distance || info.last_trip_time; + + // 날짜/시간 포맷팅 함수 + const formatDateTime = (dateStr: string) => { + if (!dateStr) return "-"; + try { + const date = new Date(dateStr); + return date.toLocaleString("ko-KR", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + } catch { + return dateStr; + } + }; + + // 거리 포맷팅 (km) + const formatDistance = (dist: number | string) => { + if (dist === null || dist === undefined) return "-"; + const num = typeof dist === "string" ? parseFloat(dist) : dist; + if (isNaN(num)) return "-"; + return `${num.toFixed(1)} km`; + }; + + // 시간 포맷팅 (분) + const formatTime = (minutes: number | string) => { + if (minutes === null || minutes === undefined) return "-"; + const num = typeof minutes === "string" ? parseInt(minutes) : minutes; + if (isNaN(num)) return "-"; + if (num < 60) return `${num}분`; + const hours = Math.floor(num / 60); + const mins = num % 60; + return mins > 0 ? `${hours}시간 ${mins}분` : `${hours}시간`; + }; + + // 데이터가 없고 아직 로드 안 했으면 로드 버튼 표시 + if (!hasEmptyTripInfo && !hasTripInfo && !tripInfo[identifier]) { + return ( +
+ +
+ ); + } + + // 데이터가 없으면 표시 안 함 + if (!hasEmptyTripInfo && !hasTripInfo) return null; + + return ( +
+ {/* 운행 정보 */} + {hasTripInfo && ( +
+
🚛 최근 운행
+
+ {(info.last_trip_start || info.last_trip_end) && ( +
+ 시간:{" "} + {formatDateTime(info.last_trip_start)} ~ {formatDateTime(info.last_trip_end)} +
+ )} +
+ {info.last_trip_distance !== undefined && info.last_trip_distance !== null && ( + + 거리:{" "} + {formatDistance(info.last_trip_distance)} + + )} + {info.last_trip_time !== undefined && info.last_trip_time !== null && ( + + 소요:{" "} + {formatTime(info.last_trip_time)} + + )} +
+ {/* 출발지/도착지 */} + {(info.departure || info.arrival) && ( +
+ {info.departure && 출발: {info.departure}} + {info.departure && info.arrival && " → "} + {info.arrival && 도착: {info.arrival}} +
+ )} +
+
+ )} + + {/* 공차 정보 */} + {hasEmptyTripInfo && ( +
+
📦 최근 공차
+
+ {(info.last_empty_start || info.last_empty_end) && ( +
+ 시간:{" "} + {formatDateTime(info.last_empty_start)} ~ {formatDateTime(info.last_empty_end)} +
+ )} +
+ {info.last_empty_distance !== undefined && info.last_empty_distance !== null && ( + + 거리:{" "} + {formatDistance(info.last_empty_distance)} + + )} + {info.last_empty_time !== undefined && info.last_empty_time !== null && ( + + 소요:{" "} + {formatTime(info.last_empty_time)} + + )} +
+
+
+ )} +
+ ); + } catch { + return null; + } + })()} + {/* 좌표 */}
{marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}