/** * 운송 통계 위젯 * - 총 운송량 (톤) * - 누적 거리 (km) * - 정시 도착률 (%) * - 쿼리 결과 기반 통계 계산 */ "use client"; import { useState, useEffect } from "react"; import { DashboardElement } from "@/components/admin/dashboard/types"; import { getApiUrl } from "@/lib/utils/apiUrl"; interface TransportStatsWidgetProps { element?: DashboardElement; refreshInterval?: number; } interface StatItem { label: string; value: number; unit: string; color: string; icon?: string; } interface StatsData { total_count: number; total_weight: number; total_distance: number; on_time_rate: number; avg_delivery_time: number; [key: string]: number; // 동적 속성 추가 } export default function TransportStatsWidget({ element, refreshInterval = 60000 }: TransportStatsWidgetProps) { const [stats, setStats] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // 데이터 로드 const loadData = async () => { try { setIsLoading(true); setError(null); // 쿼리가 설정되어 있지 않으면 안내 메시지만 표시 if (!element?.dataSource?.query) { setError("쿼리를 설정해주세요"); setIsLoading(false); return; } // 쿼리 실행하여 통계 계산 const token = localStorage.getItem("authToken"); const response = await fetch(getApiUrl("/api/dashboards/execute-query"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ query: element.dataSource.query, connectionType: element.dataSource.connectionType || "current", externalConnectionId: element.dataSource.externalConnectionId, }), }); if (!response.ok) throw new Error("데이터 로딩 실패"); const result = await response.json(); if (!result.success || !result.data?.rows) { throw new Error(result.message || "데이터 로드 실패"); } const data = result.data.rows || []; if (data.length === 0) { setStats({ total_count: 0, total_weight: 0, total_distance: 0, on_time_rate: 0, avg_delivery_time: 0 }); return; } // 자동으로 숫자 컬럼 감지 및 합계 계산 const firstRow = data[0]; const numericColumns: { [key: string]: number } = {}; // 모든 컬럼을 순회하며 숫자 컬럼 찾기 Object.keys(firstRow).forEach((key) => { const value = firstRow[key]; // 숫자로 변환 가능한 컬럼만 선택 if (value !== null && !isNaN(parseFloat(value))) { numericColumns[key] = data.reduce((sum: number, item: any) => { return sum + (parseFloat(item[key]) || 0); }, 0); } }); // 특정 키워드를 포함한 컬럼 자동 매핑 const weightKeys = ["weight", "cargo_weight", "total_weight", "중량", "무게"]; const distanceKeys = ["distance", "total_distance", "거리", "주행거리"]; const onTimeKeys = ["is_on_time", "on_time", "onTime", "정시", "정시도착"]; const deliveryTimeKeys = [ "delivery_duration", "delivery_time", "duration", "배송시간", "소요시간", "배송소요시간", ]; // 총 운송량 찾기 let total_weight = 0; for (const key of Object.keys(numericColumns)) { if (weightKeys.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) { total_weight = numericColumns[key]; break; } } // 누적 거리 찾기 let total_distance = 0; for (const key of Object.keys(numericColumns)) { if (distanceKeys.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) { total_distance = numericColumns[key]; break; } } // 정시 도착률 계산 let on_time_rate = 0; for (const key of Object.keys(firstRow)) { if (onTimeKeys.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) { const onTimeItems = data.filter((item: any) => { const onTime = item[key]; return onTime !== null && onTime !== undefined; }); if (onTimeItems.length > 0) { const onTimeCount = onTimeItems.filter((item: any) => { const onTime = item[key]; return onTime === true || onTime === "true" || onTime === 1 || onTime === "1"; }).length; on_time_rate = (onTimeCount / onTimeItems.length) * 100; } break; } } // 평균 배송시간 계산 let avg_delivery_time = 0; // 1. 먼저 배송시간 컬럼이 있는지 확인 let foundTimeColumn = false; for (const key of Object.keys(numericColumns)) { if (deliveryTimeKeys.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) { const validItems = data.filter((item: any) => { const time = parseFloat(item[key]); return !isNaN(time) && time > 0; }); if (validItems.length > 0) { const totalTime = validItems.reduce((sum: number, item: any) => { return sum + parseFloat(item[key]); }, 0); avg_delivery_time = totalTime / validItems.length; foundTimeColumn = true; } break; } } // 2. 배송시간 컬럼이 없으면 날짜 컬럼에서 자동 계산 if (!foundTimeColumn) { const startTimeKeys = ["created_at", "start_time", "departure_time", "출발시간", "시작시간"]; const endTimeKeys = [ "actual_delivery", "end_time", "arrival_time", "도착시간", "완료시간", "estimated_delivery", ]; let startKey = null; let endKey = null; // 시작 시간 컬럼 찾기 for (const key of Object.keys(firstRow)) { if (startTimeKeys.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) { startKey = key; break; } } // 종료 시간 컬럼 찾기 for (const key of Object.keys(firstRow)) { if (endTimeKeys.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) { endKey = key; break; } } // 두 컬럼이 모두 있으면 시간 차이 계산 if (startKey && endKey) { const validItems = data.filter((item: any) => { return item[startKey] && item[endKey]; }); if (validItems.length > 0) { const totalMinutes = validItems.reduce((sum: number, item: any) => { const start = new Date(item[startKey]).getTime(); const end = new Date(item[endKey]).getTime(); const diffMinutes = (end - start) / (1000 * 60); // 밀리초 -> 분 return sum + (diffMinutes > 0 ? diffMinutes : 0); }, 0); avg_delivery_time = totalMinutes / validItems.length; } } } const calculatedStats: StatsData = { total_count: data.length, // 총 건수 total_weight, total_distance, on_time_rate, avg_delivery_time, }; setStats(calculatedStats); } catch (err) { console.error("통계 로드 실패:", err); setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다"); } finally { setIsLoading(false); } }; useEffect(() => { loadData(); const interval = setInterval(loadData, refreshInterval); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, [refreshInterval, element?.dataSource]); if (isLoading && !stats) { return (
로딩 중...
); } if (error || !stats) { return (
⚠️
{error || "데이터 없음"}
{!element?.dataSource?.query &&
쿼리를 설정하세요
}
); } return (
{/* 총 건수 */}
총 건수
{stats.total_count.toLocaleString()}
{/* 총 운송량 */}
총 운송량
{stats.total_weight.toFixed(1)}
{/* 누적 거리 */}
누적 거리
{stats.total_distance.toFixed(1)} km
{/* 정시 도착률 */}
정시 도착률
{stats.on_time_rate.toFixed(1)} %
{/* 평균 배송시간 */}
평균 배송시간
{stats.avg_delivery_time.toFixed(1)}
); }