최근이동한 내역들

This commit is contained in:
leeheejin 2025-12-10 13:48:57 +09:00
parent 3608d9f9c3
commit c64c94c07b
1 changed files with 219 additions and 3 deletions

View File

@ -103,6 +103,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const [routeLoading, setRouteLoading] = useState(false);
const [routeDate, setRouteDate] = useState<string>(new Date().toISOString().split("T")[0]); // YYYY-MM-DD 형식
// 공차/운행 정보 상태
const [tripInfo, setTripInfo] = useState<Record<string, any>>({});
const [tripInfoLoading, setTripInfoLoading] = useState<string | null>(null);
// Popup 열림 상태 (자동 새로고침 일시 중지용)
const [isPopupOpen, setIsPopupOpen] = useState(false);
// 지역 필터 상태
const [selectedRegion, setSelectedRegion] = useState<string>("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(() => {
// 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),
}}
>
<Popup>
<div className="min-w-[200px]">
@ -1621,7 +1680,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
}
return (
<Marker key={marker.id} position={[marker.lat, marker.lng]} icon={markerIcon}>
<Marker
key={marker.id}
position={[marker.lat, marker.lng]}
icon={markerIcon}
eventHandlers={{
popupopen: () => setIsPopupOpen(true),
popupclose: () => setIsPopupOpen(false),
}}
>
<Popup maxWidth={350}>
<div className="max-w-[350px] min-w-[250px]" dir="ltr">
{/* 데이터 소스명만 표시 */}
@ -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 (
<div className="border-t pt-2 mt-2">
<button
onClick={() => loadTripInfo(identifier)}
disabled={tripInfoLoading === identifier}
className="w-full rounded bg-gray-100 px-2 py-1.5 text-xs text-gray-700 hover:bg-gray-200 disabled:opacity-50"
>
{tripInfoLoading === identifier ? "로딩 중..." : "📊 운행/공차 정보 보기"}
</button>
</div>
);
}
// 데이터가 없으면 표시 안 함
if (!hasEmptyTripInfo && !hasTripInfo) return null;
return (
<div className="border-t pt-2 mt-2">
{/* 운행 정보 */}
{hasTripInfo && (
<div className="mb-2">
<div className="text-xs font-semibold text-blue-600 mb-1">🚛 </div>
<div className="bg-blue-50 rounded p-2 space-y-1">
{(info.last_trip_start || info.last_trip_end) && (
<div className="text-[10px] text-gray-600">
<span className="font-medium">:</span>{" "}
{formatDateTime(info.last_trip_start)} ~ {formatDateTime(info.last_trip_end)}
</div>
)}
<div className="flex gap-3 text-[10px]">
{info.last_trip_distance !== undefined && info.last_trip_distance !== null && (
<span>
<span className="font-medium text-gray-600">:</span>{" "}
<span className="text-blue-700 font-semibold">{formatDistance(info.last_trip_distance)}</span>
</span>
)}
{info.last_trip_time !== undefined && info.last_trip_time !== null && (
<span>
<span className="font-medium text-gray-600">:</span>{" "}
<span className="text-blue-700 font-semibold">{formatTime(info.last_trip_time)}</span>
</span>
)}
</div>
{/* 출발지/도착지 */}
{(info.departure || info.arrival) && (
<div className="text-[10px] text-gray-600 pt-1 border-t border-blue-100">
{info.departure && <span>: {info.departure}</span>}
{info.departure && info.arrival && " → "}
{info.arrival && <span>: {info.arrival}</span>}
</div>
)}
</div>
</div>
)}
{/* 공차 정보 */}
{hasEmptyTripInfo && (
<div>
<div className="text-xs font-semibold text-orange-600 mb-1">📦 </div>
<div className="bg-orange-50 rounded p-2 space-y-1">
{(info.last_empty_start || info.last_empty_end) && (
<div className="text-[10px] text-gray-600">
<span className="font-medium">:</span>{" "}
{formatDateTime(info.last_empty_start)} ~ {formatDateTime(info.last_empty_end)}
</div>
)}
<div className="flex gap-3 text-[10px]">
{info.last_empty_distance !== undefined && info.last_empty_distance !== null && (
<span>
<span className="font-medium text-gray-600">:</span>{" "}
<span className="text-orange-700 font-semibold">{formatDistance(info.last_empty_distance)}</span>
</span>
)}
{info.last_empty_time !== undefined && info.last_empty_time !== null && (
<span>
<span className="font-medium text-gray-600">:</span>{" "}
<span className="text-orange-700 font-semibold">{formatTime(info.last_empty_time)}</span>
</span>
)}
</div>
</div>
</div>
)}
</div>
);
} catch {
return null;
}
})()}
{/* 좌표 */}
<div className="text-muted-foreground border-t pt-2 text-[10px]">
{marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}