207 lines
8.3 KiB
TypeScript
207 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { RefreshCw, Truck, Navigation, Gauge } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { getApiUrl } from "@/lib/utils/apiUrl";
|
|
|
|
interface Vehicle {
|
|
id: string;
|
|
vehicle_number: string;
|
|
vehicle_name: string;
|
|
driver_name: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
status: string;
|
|
speed: number;
|
|
destination: string;
|
|
}
|
|
|
|
interface VehicleListWidgetProps {
|
|
element?: any; // 대시보드 요소 (dataSource 포함)
|
|
refreshInterval?: number;
|
|
}
|
|
|
|
export default function VehicleListWidget({ element, refreshInterval = 30000 }: VehicleListWidgetProps) {
|
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
|
const [selectedStatus, setSelectedStatus] = useState<string>("all");
|
|
|
|
const loadVehicles = async () => {
|
|
setIsLoading(true);
|
|
|
|
// 설정된 쿼리가 없으면 로딩 중단 (기본 쿼리 사용 안 함)
|
|
if (!element?.dataSource?.query) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const query = element.dataSource.query;
|
|
|
|
try {
|
|
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) {
|
|
setVehicles(result.data.rows);
|
|
setLastUpdate(new Date());
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("차량 목록 로드 실패:", error);
|
|
}
|
|
|
|
setIsLoading(false);
|
|
};
|
|
|
|
// 데이터 로드 및 자동 새로고침
|
|
useEffect(() => {
|
|
loadVehicles();
|
|
const interval = setInterval(loadVehicles, refreshInterval);
|
|
return () => clearInterval(interval);
|
|
}, [element?.dataSource?.query, refreshInterval]);
|
|
|
|
// 설정되지 않았을 때도 빈 상태로 표시 (안내 메시지 제거)
|
|
|
|
const getStatusColor = (status: string) => {
|
|
const s = status?.toLowerCase() || "";
|
|
if (s === "active" || s === "running") return "bg-success";
|
|
if (s === "inactive" || s === "idle") return "bg-warning/100";
|
|
if (s === "maintenance") return "bg-warning";
|
|
if (s === "warning" || s === "breakdown") return "bg-destructive";
|
|
return "bg-muted0";
|
|
};
|
|
|
|
const getStatusText = (status: string) => {
|
|
const s = status?.toLowerCase() || "";
|
|
if (s === "active" || s === "running") return "운행 중";
|
|
if (s === "inactive" || s === "idle") return "대기";
|
|
if (s === "maintenance") return "정비";
|
|
if (s === "warning" || s === "breakdown") return "고장";
|
|
return "알 수 없음";
|
|
};
|
|
|
|
const filteredVehicles =
|
|
selectedStatus === "all" ? vehicles : vehicles.filter((v) => v.status?.toLowerCase() === selectedStatus);
|
|
|
|
return (
|
|
<div className="flex h-full w-full flex-col bg-gradient-to-br from-background to-primary/10 p-4">
|
|
{/* 헤더 */}
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-foreground">차량 목록</h3>
|
|
<p className="text-xs text-muted-foreground">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={loadVehicles} disabled={isLoading} className="h-8 w-8 p-0">
|
|
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 필터 버튼 */}
|
|
<div className="mb-3 flex gap-2 overflow-x-auto">
|
|
<button
|
|
onClick={() => setSelectedStatus("all")}
|
|
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
|
selectedStatus === "all" ? "bg-gray-900 text-white" : "bg-background text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
전체 ({vehicles.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setSelectedStatus("active")}
|
|
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
|
selectedStatus === "active" ? "bg-success text-white" : "bg-background text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
운행 중 ({vehicles.filter((v) => v.status?.toLowerCase() === "active").length})
|
|
</button>
|
|
<button
|
|
onClick={() => setSelectedStatus("inactive")}
|
|
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
|
selectedStatus === "inactive" ? "bg-warning/100 text-white" : "bg-background text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
대기 ({vehicles.filter((v) => v.status?.toLowerCase() === "inactive").length})
|
|
</button>
|
|
<button
|
|
onClick={() => setSelectedStatus("maintenance")}
|
|
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
|
selectedStatus === "maintenance" ? "bg-warning text-white" : "bg-background text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
정비 ({vehicles.filter((v) => v.status?.toLowerCase() === "maintenance").length})
|
|
</button>
|
|
<button
|
|
onClick={() => setSelectedStatus("warning")}
|
|
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
|
selectedStatus === "warning" ? "bg-destructive text-white" : "bg-background text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
고장 ({vehicles.filter((v) => v.status?.toLowerCase() === "warning").length})
|
|
</button>
|
|
</div>
|
|
|
|
{/* 차량 목록 */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
{filteredVehicles.length === 0 ? (
|
|
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-border bg-background">
|
|
<div className="text-center">
|
|
<Truck className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
<p className="mt-2 text-sm text-muted-foreground">차량이 없습니다</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{filteredVehicles.map((vehicle) => (
|
|
<div
|
|
key={vehicle.id}
|
|
className="rounded-lg border border-border bg-background p-3 shadow-sm transition-all hover:shadow-md"
|
|
>
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Truck className="h-4 w-4 text-foreground" />
|
|
<span className="font-semibold text-foreground">{vehicle.vehicle_name}</span>
|
|
</div>
|
|
<span
|
|
className={`rounded-full px-2 py-0.5 text-xs font-semibold text-white ${getStatusColor(vehicle.status)}`}
|
|
>
|
|
{getStatusText(vehicle.status)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-1 text-xs text-foreground">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">차량번호</span>
|
|
<span className="font-mono font-medium">{vehicle.vehicle_number}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">기사</span>
|
|
<span className="font-medium">{vehicle.driver_name || "미배정"}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Navigation className="h-3 w-3 text-muted-foreground" />
|
|
<span className="flex-1 truncate text-foreground">{vehicle.destination || "대기 중"}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Gauge className="h-3 w-3 text-muted-foreground" />
|
|
<span className="text-foreground">{vehicle.speed || 0} km/h</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|