Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 87 additions & 21 deletions src/app/monitoring/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -51,6 +52,8 @@ interface VehicleSidebarProps {
currentPositions: CurrentCarPosition[];
showRoute: boolean;
setShowRoute: React.Dispatch<React.SetStateAction<boolean>>;
selectedVehicleDetails: Vehicle | null;
storeVehicles: Vehicle[];
}

function VehicleSidebar({
Expand All @@ -61,7 +64,9 @@ function VehicleSidebar({
carLocations,
currentPositions,
showRoute,
setShowRoute
setShowRoute,
selectedVehicleDetails,
storeVehicles
}: VehicleSidebarProps) {
const [searchTerm, setSearchTerm] = useState('');
const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null);
Expand All @@ -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 (
<div className={`w-80 ${currentTheme.cardBg} ${currentTheme.border} rounded-lg shadow-sm overflow-hidden h-[calc(100vh-160px)] flex flex-col`}>

Expand Down Expand Up @@ -170,9 +226,16 @@ function VehicleSidebar({
</div>

<div className={`${currentTheme.border} border rounded-md p-3 mb-3`}>
<div className="grid grid-cols-1 gap-2 text-sm">
<div className="grid grid-cols-1 gap-3 text-sm">
<div className="flex items-center justify-between">
<div className={`${currentTheme.mutedText}`}>차량 상태</div>
<div className={`inline-flex items-center px-3 py-1 rounded-full ${stateDisplay.bgColor} ${stateDisplay.color} font-medium text-xs`}>
{stateDisplay.text}
</div>
</div>

<div>
<div className={`${currentTheme.mutedText} mb-1`}>현재 위치</div>
<div className={`${currentTheme.mutedText} mb-1.5`}>현재 위치</div>
<div className={`${currentTheme.textColor} grid grid-cols-2 gap-1`}>
<div>
<span className="text-xs text-gray-500">위도: </span>
Expand All @@ -184,8 +247,9 @@ function VehicleSidebar({
</div>
</div>
</div>
<div className="mt-2">
<div className={`${currentTheme.mutedText} mb-1`}>시간</div>

<div>
<div className={`${currentTheme.mutedText} mb-1.5`}>시간</div>
<div className={`${currentTheme.textColor}`}>
{new Date(selectedVehicleData.currentLocation.timestamp).toLocaleTimeString()}
</div>
Expand Down Expand Up @@ -222,7 +286,6 @@ export default function MonitoringPage() {
const [showRoute, setShowRoute] = useState(false);
const [routePoints, setRoutePoints] = useState<RouteGroup[]>([]);
const wsRef = useRef<WebSocket | null>(null);
//Todo: 회사 아이디 받아오기
const [companyId, setCompanyId] = useState<string>('1');
const [selectedCars, setSelectedCars] = useState<string[]>([]);
const [showAllCars, setShowAllCars] = useState(true);
Expand All @@ -236,7 +299,7 @@ export default function MonitoringPage() {
longitude: 127.5,
zoom: 7,
followVehicle: false,
userSetPosition: true // 사용자가 직접 설정한 위치인지 여부
userSetPosition: true
});

const [lastDragTime, setLastDragTime] = useState<number>(0);
Expand All @@ -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<Vehicle | null>(null);

const handleMapDrag = (center: {lat: number, lng: number}, zoom: number) => {
setMapSettings(prev => ({
...prev,
Expand Down Expand Up @@ -325,12 +392,9 @@ export default function MonitoringPage() {
}
}, [carLocations]);

// 연결 및 데이터 수신 처리
useEffect(() => {
connectWebSocket();

// 테스트 데이터 생성은 제거 (직접 입력 기능으로 대체)

return () => {
if (wsRef.current) {
wsRef.current.close();
Expand All @@ -341,7 +405,6 @@ export default function MonitoringPage() {
};
}, []);

// WebSocket 연결 상태가 변경될 때 자동 구독
useEffect(() => {
if (wsConnected && companyId) {
console.log('WebSocket 연결됨, 자동 구독 시도:', companyId);
Expand Down Expand Up @@ -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) {
Expand All @@ -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]);
Expand All @@ -497,7 +562,6 @@ export default function MonitoringPage() {
}
}));

// 차량 위치가 업데이트될 때마다 지도 위치 업데이트
useEffect(() => {
if (!mapSettings.followVehicle) {
return;
Expand Down Expand Up @@ -606,6 +670,8 @@ export default function MonitoringPage() {
currentPositions={currentPositions}
showRoute={showRoute}
setShowRoute={setShowRoute}
selectedVehicleDetails={selectedVehicleDetails}
storeVehicles={storeVehicles}
/>

<div className={`flex-1 ${currentTheme.cardBg} ${currentTheme.border} rounded-lg shadow-sm overflow-hidden`}>
Expand Down
34 changes: 24 additions & 10 deletions src/components/logs/VehicleLogDetailSlidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 23 additions & 13 deletions src/components/vehicles/VehicleDetailSlidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -493,14 +493,24 @@ export default function VehicleDetailSlidePanel({ isOpen, onClose, vehicle }: Ve
</div>
</div>
<div className="relative rounded-xl overflow-hidden shadow-lg">
<VehicleLocationMap
latitude={latestPosition?.latitude || 37.5666805}
longitude={latestPosition?.longitude || 126.9784147}
zoom={15}
height="400px"
isLoading={isLoadingPosition}
showLocationInfo={!!latestPosition}
/>
{latestPosition && latestPosition.latitude && latestPosition.longitude ? (
<VehicleLocationMap
latitude={latestPosition.latitude}
longitude={latestPosition.longitude}
zoom={15}
height="400px"
isLoading={isLoadingPosition}
showLocationInfo={true}
/>
) : (
<div className="flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded-xl" style={{ height: "400px" }}>
<div className="text-center p-4">
<p className="text-gray-700 dark:text-gray-300">
현재 차량 위치 정보가 없습니다
</p>
</div>
</div>
)}
</div>
</div>
</div>
Expand Down
12 changes: 8 additions & 4 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,14 @@ export const fetchApi = async <T>(endpoint: string, queryParams?: Record<string,
};

export async function fetchLatestPosition(mdn: string): Promise<{
mdn: string;
latitude: number;
longitude: number;
timestamp: string;
data: {
mdn: string;
latitude: number;
longitude: number;
timestamp: string;
};
message: string;
statusCode: number;
} | null> {
try {
const response = await fetch(`${API_BASE_URL}/api/gps/position?mdn=${mdn}`);
Expand Down
Empty file.