From 3d4a613f861fa65ca15b023b2d5d057fd76861b2 Mon Sep 17 00:00:00 2001 From: hyeonjinan096 Date: Tue, 29 Apr 2025 18:30:56 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[#85]=20feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EC=B0=A8=EB=9F=89=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=83=81=ED=83=9C=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/monitoring/page.tsx | 108 +++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index d0b7c3f..aec7bf9 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/lib/authStore'; import { useTheme } from '@/contexts/ThemeContext'; +import { useVehicleStore, Vehicle } from '@/lib/vehicleStore'; import { MinusIcon, PlusIcon, @@ -51,6 +52,8 @@ interface VehicleSidebarProps { currentPositions: CurrentCarPosition[]; showRoute: boolean; setShowRoute: React.Dispatch>; + selectedVehicleDetails: Vehicle | null; + storeVehicles: Vehicle[]; } function VehicleSidebar({ @@ -61,7 +64,9 @@ function VehicleSidebar({ carLocations, currentPositions, showRoute, - setShowRoute + setShowRoute, + selectedVehicleDetails, + storeVehicles }: VehicleSidebarProps) { const [searchTerm, setSearchTerm] = useState(''); const [selectedVehicle, setSelectedVehicle] = useState(null); @@ -81,6 +86,57 @@ function VehicleSidebar({ const selectedVehicleFullData = selectedVehicle ? carLocations.find(car => car.carId === selectedVehicle) : null; + const getStateDisplay = (state: string | null | undefined) => { + if (!state) return { + text: "알 수 없음", + color: "text-gray-500", + bgColor: "bg-gray-100 dark:bg-gray-700" + }; + + switch (state) { + case "RUNNING": + return { + text: "운행 중", + color: "text-green-600 dark:text-green-400", + bgColor: "bg-green-100 dark:bg-green-900/30" + }; + case "STOPPED": + return { + text: "정지", + color: "text-red-600 dark:text-red-400", + bgColor: "bg-red-100 dark:bg-red-900/30" + }; + case "NOT_REGISTERED": + return { + text: "미등록", + color: "text-yellow-600 dark:text-yellow-400", + bgColor: "bg-yellow-100 dark:bg-yellow-900/30" + }; + default: + return { + text: "알 수 없음", + color: "text-gray-500", + bgColor: "bg-gray-100 dark:bg-gray-700" + }; + } + }; + + const vehicleState = selectedVehicleDetails?.carState; + const stateDisplay = getStateDisplay(vehicleState); + + useEffect(() => { + if (selectedVehicle) { + console.log('선택된 차량:', selectedVehicle); + console.log('차량 상세 정보:', selectedVehicleDetails); + console.log('스토어의 모든 차량:', storeVehicles); + + const matchingVehicle = storeVehicles.find(v => + v.id === selectedVehicle || v.mdn === selectedVehicle + ); + console.log('스토어에서 찾은 차량:', matchingVehicle); + } + }, [selectedVehicle, selectedVehicleDetails, storeVehicles]); + return (
@@ -170,9 +226,16 @@ function VehicleSidebar({
-
+
+
+
차량 상태
+
+ {stateDisplay.text} +
+
+
-
현재 위치
+
현재 위치
위도: @@ -184,8 +247,9 @@ function VehicleSidebar({
-
-
시간
+ +
+
시간
{new Date(selectedVehicleData.currentLocation.timestamp).toLocaleTimeString()}
@@ -222,7 +286,6 @@ export default function MonitoringPage() { const [showRoute, setShowRoute] = useState(false); const [routePoints, setRoutePoints] = useState([]); const wsRef = useRef(null); - //Todo: 회사 아이디 받아오기 const [companyId, setCompanyId] = useState('1'); const [selectedCars, setSelectedCars] = useState([]); const [showAllCars, setShowAllCars] = useState(true); @@ -236,7 +299,7 @@ export default function MonitoringPage() { longitude: 127.5, zoom: 7, followVehicle: false, - userSetPosition: true // 사용자가 직접 설정한 위치인지 여부 + userSetPosition: true }); const [lastDragTime, setLastDragTime] = useState(0); @@ -253,6 +316,10 @@ export default function MonitoringPage() { { name: '대전', latitude: 36.3504, longitude: 127.3845, zoom: 11 } ]); + const { vehicles: storeVehicles, fetchVehicles } = useVehicleStore(); + + const [selectedVehicleDetails, setSelectedVehicleDetails] = useState(null); + const handleMapDrag = (center: {lat: number, lng: number}, zoom: number) => { setMapSettings(prev => ({ ...prev, @@ -325,12 +392,9 @@ export default function MonitoringPage() { } }, [carLocations]); - // 연결 및 데이터 수신 처리 useEffect(() => { connectWebSocket(); - // 테스트 데이터 생성은 제거 (직접 입력 기능으로 대체) - return () => { if (wsRef.current) { wsRef.current.close(); @@ -341,7 +405,6 @@ export default function MonitoringPage() { }; }, []); - // WebSocket 연결 상태가 변경될 때 자동 구독 useEffect(() => { if (wsConnected && companyId) { console.log('WebSocket 연결됨, 자동 구독 시도:', companyId); @@ -434,14 +497,12 @@ export default function MonitoringPage() { } }; - // 경로 포인트 업데이트 함수 const updateRoutePoints = () => { if (!showRoute) { setRoutePoints([]); return; } - // 각 차량별로 경로를 그룹화하여 저장 const groupedRoutes = currentPositions.map(pos => { const vehicle = carLocations.find(v => v.carId === pos.carId); if (vehicle) { @@ -462,23 +523,27 @@ export default function MonitoringPage() { setRoutePoints(groupedRoutes); }; - // 차량 선택 토글 함수 수정 - const toggleCarSelection = (carId: string) => { + const toggleCarSelection = useCallback((carId: string) => { setSelectedCars(prev => { if (prev.includes(carId)) { + setSelectedVehicleDetails(null); return prev.filter(id => id !== carId); } else { - return [...prev, carId]; + const vehicleInfo = storeVehicles.find(v => v.id === carId || v.mdn === carId); + setSelectedVehicleDetails(vehicleInfo || null); + return [carId]; } }); - }; + }, [storeVehicles]); + + useEffect(() => { + fetchVehicles(); + }, [fetchVehicles]); - // 차량 위치가 업데이트될 때마다 경로도 업데이트 useEffect(() => { updateRoutePoints(); }, [currentPositions, showRoute]); - // 경로 보기/숨기기 토글 시 경로 업데이트 useEffect(() => { updateRoutePoints(); }, [showRoute]); @@ -497,7 +562,6 @@ export default function MonitoringPage() { } })); - // 차량 위치가 업데이트될 때마다 지도 위치 업데이트 useEffect(() => { if (!mapSettings.followVehicle) { return; @@ -606,6 +670,8 @@ export default function MonitoringPage() { currentPositions={currentPositions} showRoute={showRoute} setShowRoute={setShowRoute} + selectedVehicleDetails={selectedVehicleDetails} + storeVehicles={storeVehicles} />
From da2fc353696bf47dd47cc14657c280d52573b51a Mon Sep 17 00:00:00 2001 From: hyeonjinan096 Date: Tue, 29 Apr 2025 18:51:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[#85]=20fix:=20=EC=B0=A8=EB=9F=89=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=A7=80=EB=8F=84=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=98=95=ED=83=9C=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/GpsMap.tsx | 85 +++++++++++++++++++ .../logs/VehicleLogDetailSlidePanel.tsx | 34 +++++--- 2 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 src/components/GpsMap.tsx diff --git a/src/components/GpsMap.tsx b/src/components/GpsMap.tsx new file mode 100644 index 0000000..5242e6d --- /dev/null +++ b/src/components/GpsMap.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useRef } from 'react'; +import kakao from 'kakao-maps-sdk'; + +const GpsMap: React.FC = () => { + const mapRef = useRef(null); + const polylineRef = useRef(null); + const startMarkerRef = useRef(null); + const endMarkerRef = useRef(null); + + useEffect(() => { + if (gpsData && gpsData.data && gpsData.data.route && gpsData.data.route.length > 0) { + console.log('GPS 데이터 로드됨:', gpsData.data.route.length, '개의 위치 포인트'); + + // 중복 제거: 같은 타임스탬프의 위치 데이터는 한 번만 사용 + const uniquePoints = []; + const seenTimestamps = new Set(); + + for (const point of gpsData.data.route) { + if (!seenTimestamps.has(point.timestamp)) { + uniquePoints.push(point); + seenTimestamps.add(point.timestamp); + } + } + + console.log('중복 제거 후 위치 포인트:', uniquePoints.length, '개'); + + // 지도 경계 설정을 위한 좌표 범위 계산 + if (uniquePoints.length > 0) { + const bounds = new kakao.maps.LatLngBounds(); + + // 경로 그리기 + const path = uniquePoints.map(point => { + const latLng = new kakao.maps.LatLng(point.latitude, point.longitude); + bounds.extend(latLng); + return latLng; + }); + + const polyline = new kakao.maps.Polyline({ + path: path, + strokeWeight: 5, + strokeColor: '#FF0000', + strokeOpacity: 0.7, + strokeStyle: 'solid' + }); + + // 기존 경로 삭제 + if (polylineRef.current) { + polylineRef.current.setMap(null); + } + + // 새 경로 설정 + polylineRef.current = polyline; + polyline.setMap(mapRef.current); + + // 지도 범위 조정 + mapRef.current.setBounds(bounds); + + // 시작점과 끝점 마커 표시 + if (startMarkerRef.current) startMarkerRef.current.setMap(null); + if (endMarkerRef.current) endMarkerRef.current.setMap(null); + + const startMarker = new kakao.maps.Marker({ + position: path[0], + map: mapRef.current, + title: '시작 위치' + }); + + const endMarker = new kakao.maps.Marker({ + position: path[path.length - 1], + map: mapRef.current, + title: '종료 위치' + }); + + startMarkerRef.current = startMarker; + endMarkerRef.current = endMarker; + } + } + }, [gpsData]); + + return ( +
+ ); +}; + +export default GpsMap; \ No newline at end of file diff --git a/src/components/logs/VehicleLogDetailSlidePanel.tsx b/src/components/logs/VehicleLogDetailSlidePanel.tsx index 2c894a4..24b7f5b 100644 --- a/src/components/logs/VehicleLogDetailSlidePanel.tsx +++ b/src/components/logs/VehicleLogDetailSlidePanel.tsx @@ -57,16 +57,30 @@ export default function VehicleLogDetailSlidePanel({ isOpen, onClose, log, onDel formattedEndTime ) as RouteResponse; - if (routeData && routeData.route && routeData.route.length > 0) { - const points = routeData.route.map((point: RoutePoint) => ({ - lat: point.latitude, - lng: point.longitude, - timestamp: point.timestamp - })); - setRoutePoints(points); - } else { - setRoutePoints([]); - } + // 응답 구조에 맞게 경로 데이터 추출 및 중복 제거 + const points = routeData.data?.route + ? (() => { + // 중복 제거: 같은 타임스탬프의 위치 데이터는 한 번만 사용 + const uniquePoints = []; + const seenTimestamps = new Set(); + + for (const point of routeData.data.route) { + if (!seenTimestamps.has(point.timestamp)) { + uniquePoints.push({ + lat: point.latitude, + lng: point.longitude, + timestamp: point.timestamp + }); + seenTimestamps.add(point.timestamp); + } + } + + console.log('중복 제거 후 위치 포인트:', uniquePoints.length, '개'); + return uniquePoints; + })() + : []; + + setRoutePoints(points); } catch (error) { setRoutePoints([]); } finally { From 7cd031744e32778354d31ab5bb0a2bab88929f39 Mon Sep 17 00:00:00 2001 From: hyeonjinan096 Date: Tue, 29 Apr 2025 18:57:32 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[#85]=20fix:=20GpsMap=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/GpsMap.tsx | 85 --------------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 src/components/GpsMap.tsx diff --git a/src/components/GpsMap.tsx b/src/components/GpsMap.tsx deleted file mode 100644 index 5242e6d..0000000 --- a/src/components/GpsMap.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import kakao from 'kakao-maps-sdk'; - -const GpsMap: React.FC = () => { - const mapRef = useRef(null); - const polylineRef = useRef(null); - const startMarkerRef = useRef(null); - const endMarkerRef = useRef(null); - - useEffect(() => { - if (gpsData && gpsData.data && gpsData.data.route && gpsData.data.route.length > 0) { - console.log('GPS 데이터 로드됨:', gpsData.data.route.length, '개의 위치 포인트'); - - // 중복 제거: 같은 타임스탬프의 위치 데이터는 한 번만 사용 - const uniquePoints = []; - const seenTimestamps = new Set(); - - for (const point of gpsData.data.route) { - if (!seenTimestamps.has(point.timestamp)) { - uniquePoints.push(point); - seenTimestamps.add(point.timestamp); - } - } - - console.log('중복 제거 후 위치 포인트:', uniquePoints.length, '개'); - - // 지도 경계 설정을 위한 좌표 범위 계산 - if (uniquePoints.length > 0) { - const bounds = new kakao.maps.LatLngBounds(); - - // 경로 그리기 - const path = uniquePoints.map(point => { - const latLng = new kakao.maps.LatLng(point.latitude, point.longitude); - bounds.extend(latLng); - return latLng; - }); - - const polyline = new kakao.maps.Polyline({ - path: path, - strokeWeight: 5, - strokeColor: '#FF0000', - strokeOpacity: 0.7, - strokeStyle: 'solid' - }); - - // 기존 경로 삭제 - if (polylineRef.current) { - polylineRef.current.setMap(null); - } - - // 새 경로 설정 - polylineRef.current = polyline; - polyline.setMap(mapRef.current); - - // 지도 범위 조정 - mapRef.current.setBounds(bounds); - - // 시작점과 끝점 마커 표시 - if (startMarkerRef.current) startMarkerRef.current.setMap(null); - if (endMarkerRef.current) endMarkerRef.current.setMap(null); - - const startMarker = new kakao.maps.Marker({ - position: path[0], - map: mapRef.current, - title: '시작 위치' - }); - - const endMarker = new kakao.maps.Marker({ - position: path[path.length - 1], - map: mapRef.current, - title: '종료 위치' - }); - - startMarkerRef.current = startMarker; - endMarkerRef.current = endMarker; - } - } - }, [gpsData]); - - return ( -
- ); -}; - -export default GpsMap; \ No newline at end of file From 8403ce7b2fd29d3494e17e5b066a5c79eab488fa Mon Sep 17 00:00:00 2001 From: hyeonjinan096 Date: Tue, 29 Apr 2025 19:35:13 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[#85]=20feat:=20=EC=B0=A8=EB=9F=89=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EC=9C=84=EC=B9=98=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=84=9C=EB=B2=84=20=EC=9D=91=EB=8B=B5=ED=98=95?= =?UTF-8?q?=ED=83=9C=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/vehicles/VehicleDetailSlidePanel.tsx | 10 +++++----- src/lib/api.ts | 12 ++++++++---- src/pages/api/vehicle/position.ts | 0 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 src/pages/api/vehicle/position.ts diff --git a/src/components/vehicles/VehicleDetailSlidePanel.tsx b/src/components/vehicles/VehicleDetailSlidePanel.tsx index 2e6b498..90ed0a2 100644 --- a/src/components/vehicles/VehicleDetailSlidePanel.tsx +++ b/src/components/vehicles/VehicleDetailSlidePanel.tsx @@ -38,12 +38,12 @@ export default function VehicleDetailSlidePanel({ isOpen, onClose, vehicle }: Ve setIsLoadingPosition(true); try { - const position = await fetchLatestPosition(vehicle.mdn); - if (position) { + const response = await fetchLatestPosition(vehicle.mdn); + if (response && response.data) { setLatestPosition({ - latitude: position.latitude, - longitude: position.longitude, - timestamp: position.timestamp + latitude: response.data.latitude, + longitude: response.data.longitude, + timestamp: response.data.timestamp }); } else { setLatestPosition(null); diff --git a/src/lib/api.ts b/src/lib/api.ts index c07c546..9a58d1d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -110,10 +110,14 @@ export const fetchApi = async (endpoint: string, queryParams?: Record { try { const response = await fetch(`${API_BASE_URL}/api/gps/position?mdn=${mdn}`); diff --git a/src/pages/api/vehicle/position.ts b/src/pages/api/vehicle/position.ts new file mode 100644 index 0000000..e69de29 From cfabe3a077a2790722f4ecafa01832bad1446f22 Mon Sep 17 00:00:00 2001 From: hyeonjinan096 Date: Tue, 29 Apr 2025 19:40:29 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[#85]=20feat:=20=EC=B0=A8=EB=9F=89=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=95=88=EB=96=A0=EC=A7=80=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vehicles/VehicleDetailSlidePanel.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/vehicles/VehicleDetailSlidePanel.tsx b/src/components/vehicles/VehicleDetailSlidePanel.tsx index 90ed0a2..9ca21ef 100644 --- a/src/components/vehicles/VehicleDetailSlidePanel.tsx +++ b/src/components/vehicles/VehicleDetailSlidePanel.tsx @@ -493,14 +493,24 @@ export default function VehicleDetailSlidePanel({ isOpen, onClose, vehicle }: Ve
- + {latestPosition && latestPosition.latitude && latestPosition.longitude ? ( + + ) : ( +
+
+

+ 차량 위치 정보가 없습니다 +

+
+
+ )}
From 61cfbf0b3b9df2557d18936eb9336e87fee00eb1 Mon Sep 17 00:00:00 2001 From: hyeonjinan096 Date: Tue, 29 Apr 2025 19:43:08 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[#85]=20modify:=20=EC=B0=A8=EB=9F=89=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/vehicles/VehicleDetailSlidePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/vehicles/VehicleDetailSlidePanel.tsx b/src/components/vehicles/VehicleDetailSlidePanel.tsx index 9ca21ef..d1859f3 100644 --- a/src/components/vehicles/VehicleDetailSlidePanel.tsx +++ b/src/components/vehicles/VehicleDetailSlidePanel.tsx @@ -506,7 +506,7 @@ export default function VehicleDetailSlidePanel({ isOpen, onClose, vehicle }: Ve

- 차량 위치 정보가 없습니다 + 현재 차량 위치 정보가 없습니다