201 lines
7.5 KiB
TypeScript
201 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import { RefreshCw, TrendingUp, TrendingDown } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||
|
||
interface VehicleStatusWidgetProps {
|
||
element?: any; // 대시보드 요소 (dataSource 포함)
|
||
refreshInterval?: number;
|
||
}
|
||
|
||
interface StatusData {
|
||
active: number; // 운행 중
|
||
inactive: number; // 대기
|
||
maintenance: number; // 정비
|
||
warning: number; // 고장
|
||
total: number;
|
||
}
|
||
|
||
export default function VehicleStatusWidget({ element, refreshInterval = 30000 }: VehicleStatusWidgetProps) {
|
||
const [statusData, setStatusData] = useState<StatusData>({
|
||
active: 0,
|
||
inactive: 0,
|
||
maintenance: 0,
|
||
warning: 0,
|
||
total: 0,
|
||
});
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||
|
||
const loadStatusData = 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) {
|
||
const newStatus: StatusData = {
|
||
active: 0,
|
||
inactive: 0,
|
||
maintenance: 0,
|
||
warning: 0,
|
||
total: 0,
|
||
};
|
||
|
||
// 쿼리 결과가 GROUP BY 형식인지 확인
|
||
const isGroupedData = result.data.rows[0].count !== undefined;
|
||
|
||
if (isGroupedData) {
|
||
// GROUP BY 형식: SELECT status, COUNT(*) as count
|
||
result.data.rows.forEach((row: any) => {
|
||
const count = parseInt(row.count) || 0;
|
||
const status = row.status?.toLowerCase() || "";
|
||
|
||
if (status === "active" || status === "running") {
|
||
newStatus.active = count;
|
||
} else if (status === "inactive" || status === "idle") {
|
||
newStatus.inactive = count;
|
||
} else if (status === "maintenance") {
|
||
newStatus.maintenance = count;
|
||
} else if (status === "warning" || status === "breakdown") {
|
||
newStatus.warning = count;
|
||
}
|
||
|
||
newStatus.total += count;
|
||
});
|
||
} else {
|
||
// SELECT * 형식: 전체 데이터를 가져와서 카운트
|
||
result.data.rows.forEach((row: any) => {
|
||
const status = row.status?.toLowerCase() || "";
|
||
|
||
if (status === "active" || status === "running") {
|
||
newStatus.active++;
|
||
} else if (status === "inactive" || status === "idle") {
|
||
newStatus.inactive++;
|
||
} else if (status === "maintenance") {
|
||
newStatus.maintenance++;
|
||
} else if (status === "warning" || status === "breakdown") {
|
||
newStatus.warning++;
|
||
}
|
||
|
||
newStatus.total++;
|
||
});
|
||
}
|
||
|
||
setStatusData(newStatus);
|
||
setLastUpdate(new Date());
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("차량 상태 데이터 로드 실패:", error);
|
||
}
|
||
|
||
setIsLoading(false);
|
||
};
|
||
|
||
// 데이터 로드 및 자동 새로고침
|
||
useEffect(() => {
|
||
loadStatusData();
|
||
const interval = setInterval(loadStatusData, refreshInterval);
|
||
return () => clearInterval(interval);
|
||
}, [element?.dataSource?.query, refreshInterval]);
|
||
|
||
// 설정되지 않았을 때도 빈 상태로 표시 (안내 메시지 제거)
|
||
|
||
const activeRate = statusData.total > 0 ? ((statusData.active / statusData.total) * 100).toFixed(1) : "0";
|
||
|
||
return (
|
||
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-background to-success/10 p-2">
|
||
{/* 헤더 */}
|
||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||
<div className="flex-1">
|
||
<h3 className="text-sm font-bold text-foreground">📊 차량 상태 현황</h3>
|
||
{statusData.total > 0 ? (
|
||
<p className="text-xs text-muted-foreground">{lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||
) : (
|
||
<p className="text-xs text-warning">⚙️ 데이터 연결 필요</p>
|
||
)}
|
||
</div>
|
||
<Button variant="outline" size="sm" onClick={loadStatusData} disabled={isLoading} className="h-7 w-7 p-0">
|
||
<RefreshCw className={`h-3 w-3 ${isLoading ? "animate-spin" : ""}`} />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 스크롤 가능한 콘텐츠 영역 */}
|
||
<div className="flex-1 overflow-y-auto">
|
||
{/* 총 차량 수 */}
|
||
<div className="mb-1 rounded border border-border bg-background p-1.5 shadow-sm">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-xs text-foreground">총 차량</div>
|
||
<div className="text-base font-bold text-foreground">{statusData.total}대</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-xs text-foreground">가동률</div>
|
||
<div className="flex items-center gap-0.5 text-sm font-bold text-success">{activeRate}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 상태별 카드 */}
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
{/* 운행 중 */}
|
||
<div className="rounded border-l-2 border-success bg-background p-1.5 shadow-sm">
|
||
<div className="mb-0.5 flex items-center gap-1">
|
||
<div className="h-1.5 w-1.5 rounded-full bg-success"></div>
|
||
<div className="text-xs font-medium text-foreground">운행</div>
|
||
</div>
|
||
<div className="text-lg font-bold text-success">{statusData.active}</div>
|
||
</div>
|
||
|
||
{/* 대기 */}
|
||
<div className="rounded border-l-2 border-warning bg-background p-1.5 shadow-sm">
|
||
<div className="mb-0.5 flex items-center gap-1">
|
||
<div className="h-1.5 w-1.5 rounded-full bg-warning/100"></div>
|
||
<div className="text-xs font-medium text-foreground">대기</div>
|
||
</div>
|
||
<div className="text-lg font-bold text-warning">{statusData.inactive}</div>
|
||
</div>
|
||
|
||
{/* 정비 */}
|
||
<div className="rounded border-l-2 border-warning bg-background p-1.5 shadow-sm">
|
||
<div className="mb-0.5 flex items-center gap-1">
|
||
<div className="h-1.5 w-1.5 rounded-full bg-warning"></div>
|
||
<div className="text-xs font-medium text-foreground">정비</div>
|
||
</div>
|
||
<div className="text-lg font-bold text-warning">{statusData.maintenance}</div>
|
||
</div>
|
||
|
||
{/* 고장 */}
|
||
<div className="rounded border-l-2 border-destructive bg-background p-1.5 shadow-sm">
|
||
<div className="mb-0.5 flex items-center gap-1">
|
||
<div className="h-1.5 w-1.5 rounded-full bg-destructive"></div>
|
||
<div className="text-xs font-medium text-foreground">고장</div>
|
||
</div>
|
||
<div className="text-lg font-bold text-destructive">{statusData.warning}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|