최근이동한 내역들
This commit is contained in:
parent
3608d9f9c3
commit
c64c94c07b
|
|
@ -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)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue