ERP-node/frontend/components/dashboard/widgets/ListSummaryWidget.tsx

327 lines
10 KiB
TypeScript
Raw Normal View History

/**
*
*
* merge
*/
"use client";
import React, { useState, useEffect } from "react";
import { DashboardElement } from "@/components/admin/dashboard/types";
interface ListSummaryWidgetProps {
element: DashboardElement;
}
interface ColumnInfo {
key: string;
label: string;
}
// 컬럼명 한글 번역
const translateColumnName = (colName: string): string => {
const columnTranslations: { [key: string]: string } = {
// 공통
"id": "ID",
"name": "이름",
"status": "상태",
"created_at": "생성일",
"updated_at": "수정일",
"created_date": "생성일",
"updated_date": "수정일",
// 기사 관련
"driver_id": "기사ID",
"phone": "전화번호",
"license_number": "면허번호",
"vehicle_id": "차량ID",
"current_location": "현재위치",
"rating": "평점",
"total_deliveries": "총배송건수",
"average_delivery_time": "평균배송시간",
"total_distance": "총운행거리",
"join_date": "가입일",
"last_active": "마지막활동",
// 차량 관련
"vehicle_number": "차량번호",
"model": "모델",
"year": "연식",
"color": "색상",
"type": "종류",
// 배송 관련
"delivery_id": "배송ID",
"order_id": "주문ID",
"customer_name": "고객명",
"address": "주소",
"delivery_date": "배송일",
"estimated_time": "예상시간",
// 제품 관련
"product_id": "제품ID",
"product_name": "제품명",
"price": "가격",
"stock": "재고",
"category": "카테고리",
"description": "설명",
// 주문 관련
"order_date": "주문일",
"quantity": "수량",
"total_amount": "총금액",
"payment_status": "결제상태",
// 고객 관련
"customer_id": "고객ID",
"email": "이메일",
"company": "회사",
"department": "부서",
};
return columnTranslations[colName.toLowerCase()] ||
columnTranslations[colName.replace(/_/g, '').toLowerCase()] ||
colName;
};
/**
*
* - SQL
* - (, , , )
*/
export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<ColumnInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [tableName, setTableName] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
loadData();
// 자동 새로고침 (30초마다)
const interval = setInterval(loadData, 30000);
return () => clearInterval(interval);
}, [element]);
const loadData = async () => {
if (!element?.dataSource?.query) {
setError(null);
setLoading(false);
setTableName(null);
return;
}
// 쿼리에서 테이블 이름 추출
const extractTableName = (query: string): string | null => {
const fromMatch = query.match(/FROM\s+([a-zA-Z0-9_가-힣]+)/i);
if (fromMatch) {
return fromMatch[1];
}
return null;
};
try {
setLoading(true);
const extractedTableName = extractTableName(element.dataSource.query);
setTableName(extractedTableName);
const token = localStorage.getItem("authToken");
const response = await fetch("/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",
connectionId: element.dataSource.connectionId,
}),
});
if (!response.ok) throw new Error("데이터 로딩 실패");
const result = await response.json();
if (result.success && result.data?.rows) {
const rows = result.data.rows;
// 컬럼 정보 추출 (한글 번역 적용)
if (rows.length > 0) {
const cols: ColumnInfo[] = Object.keys(rows[0]).map((key) => ({
key,
label: translateColumnName(key),
}));
setColumns(cols);
}
setData(rows);
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
} finally {
setLoading(false);
}
};
// 테이블 이름 한글 번역
const translateTableName = (name: string): string => {
const tableTranslations: { [key: string]: string } = {
"drivers": "기사",
"driver": "기사",
"vehicles": "차량",
"vehicle": "차량",
"products": "제품",
"product": "제품",
"orders": "주문",
"order": "주문",
"customers": "고객",
"customer": "고객",
"deliveries": "배송",
"delivery": "배송",
"users": "사용자",
"user": "사용자",
};
return tableTranslations[name.toLowerCase()] ||
tableTranslations[name.replace(/_/g, '').toLowerCase()] ||
name;
};
const displayTitle = tableName ? `${translateTableName(tableName)} 목록` : "데이터 목록";
// 검색 필터링
const filteredData = data.filter((row) =>
Object.values(row).some((value) =>
String(value).toLowerCase().includes(searchTerm.toLowerCase())
)
);
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
<p className="mt-2 text-sm text-gray-500"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-red-500">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
>
</button>
</div>
</div>
);
}
if (!element?.dataSource?.query) {
return (
<div className="flex h-full items-center justify-center p-3">
<div className="max-w-xs space-y-2 text-center">
<div className="text-3xl">📋</div>
<h3 className="text-sm font-bold text-gray-900"> </h3>
<div className="space-y-1.5 text-xs text-gray-600">
<p className="font-medium">📋 </p>
<ul className="space-y-0.5 text-left">
<li> SQL </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="mt-2 rounded-lg bg-blue-50 p-2 text-[10px] text-blue-700">
<p className="font-medium"> </p>
<p className="mt-0.5"> </p>
<p>SQL </p>
</div>
</div>
</div>
);
}
return (
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50 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-gray-900">📋 {displayTitle}</h3>
<p className="text-xs text-gray-500"> {filteredData.length.toLocaleString()}</p>
</div>
<button
onClick={loadData}
className="flex h-7 w-7 items-center justify-center rounded border border-border bg-white p-0 text-xs hover:bg-accent disabled:opacity-50"
disabled={loading}
>
{loading ? "⏳" : "🔄"}
</button>
</div>
{/* 검색 */}
{data.length > 0 && (
<div className="mb-2 flex-shrink-0">
<input
type="text"
placeholder="검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
)}
{/* 테이블 */}
<div className="flex-1 overflow-auto">
{filteredData.length > 0 ? (
<table className="w-full border-collapse text-xs">
<thead className="sticky top-0 bg-gray-100">
<tr>
{columns.map((col) => (
<th
key={col.key}
className="border border-gray-300 px-2 py-1 text-left font-semibold text-gray-700"
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody className="bg-white">
{filteredData.map((row, idx) => (
<tr key={idx} className="hover:bg-gray-50">
{columns.map((col) => (
<td
key={col.key}
className="border border-gray-300 px-2 py-1 text-gray-800"
>
{String(row[col.key] || "")}
</td>
))}
</tr>
))}
</tbody>
</table>
) : (
<div className="flex h-full items-center justify-center text-gray-500">
<p className="text-sm"> </p>
</div>
)}
</div>
</div>
);
}