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
118 changes: 90 additions & 28 deletions src/app/monitoring/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ interface VehicleSidebarProps {
currentTheme: any;
carLocations: CarLocation[];
currentPositions: CurrentCarPosition[];
showRoute: boolean;
setShowRoute: React.Dispatch<React.SetStateAction<boolean>>;
}

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

<div className={`p-4 ${currentTheme.border} border-b`}>
<div className="flex items-center gap-1.5 mb-3">
<TruckIcon className={`h-5 w-5 ${currentTheme.iconColor}`} />
<h2 className={`text-lg font-bold ${currentTheme.headingText}`}>
차량 목록
<span className={`ml-2 text-sm font-normal ${currentTheme.mutedText}`}>
{vehicles.length}대
</span>
</h2>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-1.5">
<TruckIcon className={`h-5 w-5 ${currentTheme.iconColor}`} />
<h2 className={`text-lg font-bold ${currentTheme.headingText}`}>
차량 목록
<span className={`ml-2 text-sm font-normal ${currentTheme.mutedText}`}>
{vehicles.length}대
</span>
</h2>
</div>
<button
onClick={() => setShowRoute(!showRoute)}
className={`px-3 py-1 rounded-md text-sm ${
showRoute
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white'
}`}
>
{showRoute ? '경로 숨기기' : '경로 보기'}
</button>
</div>

<div className={`relative`}>
Expand Down Expand Up @@ -190,15 +206,24 @@ function VehicleSidebar({
);
}

interface RouteGroup {
carId: string;
color: string;
points: { lat: number; lng: number }[];
}

export default function MonitoringPage() {
const router = useRouter();
const { isAuthenticated } = useAuthStore();
const { currentTheme } = useTheme();
const [wsConnected, setWsConnected] = useState(false);
const [carLocations, setCarLocations] = useState<CarLocation[]>([]);
const [currentPositions, setCurrentPositions] = useState<CurrentCarPosition[]>([]);
const [showRoute, setShowRoute] = useState(false);
const [routePoints, setRoutePoints] = useState<RouteGroup[]>([]);
const wsRef = useRef<WebSocket | null>(null);
const [companyId, setCompanyId] = useState<string>('2');
//Todo: 회사 아이디 받아오기
const [companyId, setCompanyId] = useState<string>('1');
const [selectedCars, setSelectedCars] = useState<string[]>([]);
const [showAllCars, setShowAllCars] = useState(true);
const animationRef = useRef<NodeJS.Timeout | null>(null);
Expand All @@ -210,10 +235,10 @@ export default function MonitoringPage() {
latitude: 36.5,
longitude: 127.5,
zoom: 7,
followVehicle: false,
followVehicle: false,
userSetPosition: true // 사용자가 직접 설정한 위치인지 여부
});


const [lastDragTime, setLastDragTime] = useState<number>(0);
const dragCooldownMs = 5000;

Expand All @@ -229,15 +254,12 @@ export default function MonitoringPage() {
]);

const handleMapDrag = (center: {lat: number, lng: number}, zoom: number) => {

setLastDragTime(Date.now());


setMapSettings(prev => ({
...prev,
latitude: center.lat,
longitude: center.lng,
zoom: zoom
zoom: zoom,
userSetPosition: true
}));
};

Expand All @@ -251,7 +273,8 @@ export default function MonitoringPage() {
const toggleVehicleFollow = () => {
setMapSettings(prev => ({
...prev,
followVehicle: !prev.followVehicle
followVehicle: !prev.followVehicle,
userSetPosition: false
}));
};

Expand Down Expand Up @@ -411,6 +434,35 @@ 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) {
return {
carId: pos.carId,
color: pos.color,
points: vehicle.locations
.slice(0, pos.currentIndex + 1)
.map(loc => ({
lat: loc.latitude,
lng: loc.longitude
}))
};
}
return null;
}).filter((route): route is RouteGroup => route !== null);

setRoutePoints(groupedRoutes);
};

// 차량 선택 토글 함수 수정
const toggleCarSelection = (carId: string) => {
setSelectedCars(prev => {
if (prev.includes(carId)) {
Expand All @@ -421,6 +473,16 @@ export default function MonitoringPage() {
});
};

// 차량 위치가 업데이트될 때마다 경로도 업데이트
useEffect(() => {
updateRoutePoints();
}, [currentPositions, showRoute]);

// 경로 보기/숨기기 토글 시 경로 업데이트
useEffect(() => {
updateRoutePoints();
}, [showRoute]);

const visibleMarkers = currentPositions
.filter(pos => showAllCars || selectedCars.includes(pos.carId))
.map(pos => ({
Expand All @@ -435,28 +497,25 @@ export default function MonitoringPage() {
}
}));

// 차량 위치가 업데이트될 때마다 지도 위치 업데이트
useEffect(() => {
if (!mapSettings.followVehicle) {
return;
}

const timeSinceDrag = Date.now() - lastDragTime;
if (timeSinceDrag < dragCooldownMs) {
return;
}

if (selectedCars.length === 1) {
const selectedCar = currentPositions.find(pos => pos.carId === selectedCars[0]);
if (selectedCar) {
setMapSettings(prev => ({
...prev,
latitude: selectedCar.currentLocation.latitude,
longitude: selectedCar.currentLocation.longitude,
zoom: 15
zoom: 15,
userSetPosition: false
}));
}
}
}, [currentPositions, selectedCars, mapSettings.followVehicle, lastDragTime]);
}, [currentPositions, selectedCars, mapSettings.followVehicle]);

const handleSubscribe = () => {
if (wsRef.current && companyId && wsConnected) {
Expand Down Expand Up @@ -536,7 +595,6 @@ export default function MonitoringPage() {
</div>
</div>


<div className="container mx-auto px-6 pb-4">
<div className="flex gap-6">
<VehicleSidebar
Expand All @@ -546,10 +604,11 @@ export default function MonitoringPage() {
currentTheme={currentTheme}
carLocations={carLocations}
currentPositions={currentPositions}
showRoute={showRoute}
setShowRoute={setShowRoute}
/>

<div className={`flex-1 ${currentTheme.cardBg} ${currentTheme.border} rounded-lg shadow-sm overflow-hidden`}>

<div className={`flex justify-between items-center px-4 py-3 ${currentTheme.border} border-b`}>
<div className="flex items-center gap-3">
<div className={`flex items-center ${currentTheme.cardBg} rounded-md shadow-sm ${currentTheme.border} p-1`}>
Expand Down Expand Up @@ -624,6 +683,9 @@ export default function MonitoringPage() {
markers={visibleMarkers}
onMapDrag={handleMapDrag}
allowDrag={true}
showRoute={showRoute}
routePoints={routePoints}
followVehicle={mapSettings.followVehicle}
/>
</div>

Expand All @@ -640,4 +702,4 @@ export default function MonitoringPage() {
</div>
</div>
);
}
}
71 changes: 67 additions & 4 deletions src/components/map/CarMap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { NAVER_CLIENT_ID } from '@/lib/env';

interface RoutePoint {
lat: number;
Expand All @@ -17,6 +16,12 @@ interface MarkerData {
onClick?: (vehicleId: string) => void;
}

interface RouteGroup {
carId: string;
color: string;
points: { lat: number; lng: number }[];
}

interface NaverMapProps {
// 기본 지도 속성
latitude?: number;
Expand All @@ -28,9 +33,14 @@ interface NaverMapProps {
// 마커 관련 속성
markers?: MarkerData[];

// 경로 관련 속성
showRoute?: boolean;
routePoints?: RouteGroup[];

// 단순화된 속성들
allowDrag?: boolean; // 드래그 허용 (기본값: true)
onMapDrag?: (center: {lat: number, lng: number}, zoom: number) => void; // 지도 드래그 콜백
followVehicle?: boolean; // 차량 추적 여부
}

declare global {
Expand Down Expand Up @@ -66,9 +76,8 @@ const loadNaverScript = (): Promise<void> => {
const script = document.createElement('script');
script.id = 'naver-map-script';
script.type = 'text/javascript';

// 환경변수에서 CLIENT ID 가져오기
const clientId = NAVER_CLIENT_ID || 'xefwc1thif';
const clientId = process.env.NEXT_PUBLIC_NAVER_CLIENT_ID;

script.src = `https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=${clientId}`;

Expand All @@ -95,12 +104,16 @@ export default function CarMap({
width = '100%',
zoom = 15,
markers = [],
showRoute = false,
routePoints = [],
allowDrag = true,
onMapDrag
onMapDrag,
followVehicle = false
}: NaverMapProps) {
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapInstance = useRef<any>(null);
const markersRef = useRef<any[]>([]);
const routeLinesRef = useRef<{[key: string]: any}>({}); // 각 차량의 경로를 개별적으로 관리
const [isRefReady, setIsRefReady] = useState(false);
const [isMapLoaded, setIsMapLoaded] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -331,6 +344,56 @@ export default function CarMap({
}
}, [markers]);

// 경로 업데이트 효과
useEffect(() => {
if (mapInstance.current && window.naver && showRoute && routePoints.length > 0) {
// 기존 경로 제거
Object.values(routeLinesRef.current).forEach(line => {
if (line) line.setMap(null);
});
routeLinesRef.current = {};

// 새 경로 생성
const drawRoutes = () => {
routePoints.forEach(routeGroup => {
const path = routeGroup.points.map(point =>
new window.naver.maps.LatLng(point.lat, point.lng)
);

routeLinesRef.current[routeGroup.carId] = new window.naver.maps.Polyline({
path: path,
strokeColor: routeGroup.color,
strokeWeight: 3,
strokeOpacity: 0.8,
map: mapInstance.current
});
});
};

drawRoutes();
} else {
// 경로 표시가 꺼졌을 때 모든 경로 제거
Object.values(routeLinesRef.current).forEach(line => {
if (line) line.setMap(null);
});
routeLinesRef.current = {};
}
}, [showRoute, routePoints, isMapLoaded]);

// 차량 추적 효과
useEffect(() => {
if (mapInstance.current && window.naver && followVehicle && markers.length > 0) {
const selectedMarker = markers.find(marker => marker.isSelected);
if (selectedMarker) {
// 선택된 차량의 위치로 지도 중심 이동
const position = new window.naver.maps.LatLng(selectedMarker.lat, selectedMarker.lng);
mapInstance.current.setCenter(position);
// 줌 레벨은 고정 (기본값 15)
mapInstance.current.setZoom(15);
}
}
}, [followVehicle, markers, isMapLoaded]);

if (error) {
return (
<div className="flex items-center justify-center h-[400px] bg-gray-100 dark:bg-gray-800 rounded-xl">
Expand Down
2 changes: 1 addition & 1 deletion src/components/map/NaverMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const loadNaverScript = (): Promise<void> => {
script.type = 'text/javascript';

// 환경변수에서 CLIENT ID 가져오기
const clientId = NAVER_CLIENT_ID || 'xefwc1thif';
const clientId = process.env.NEXT_PUBLIC_NAVER_CLIENT_ID;

console.log(`NaverMap: 사용 clientId=${clientId}`);

Expand Down
Empty file.