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

327 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ⚠️ 임시 주석 처리된 파일
* 다른 분이 범용 리스트 작업 중이어서 충돌 방지를 위해 주석 처리
* 나중에 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>
);
}