2025-10-15 16:16:27 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* ⚠️ 임시 주석 처리된 파일
|
|
|
|
|
|
* 다른 분이 범용 리스트 작업 중이어서 충돌 방지를 위해 주석 처리
|
|
|
|
|
|
* 나중에 merge 시 활성화 필요
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import { DashboardElement } from "@/components/admin/dashboard/types";
|
2025-10-24 16:08:57 +09:00
|
|
|
|
import { getApiUrl } from "@/lib/utils/apiUrl";
|
2025-10-15 16:16:27 +09:00
|
|
|
|
|
|
|
|
|
|
interface ListSummaryWidgetProps {
|
|
|
|
|
|
element: DashboardElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ColumnInfo {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼명 한글 번역
|
|
|
|
|
|
const translateColumnName = (colName: string): string => {
|
|
|
|
|
|
const columnTranslations: { [key: string]: string } = {
|
|
|
|
|
|
// 공통
|
2025-10-22 13:40:15 +09:00
|
|
|
|
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: "마지막활동",
|
|
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 차량 관련
|
2025-10-22 13:40:15 +09:00
|
|
|
|
vehicle_number: "차량번호",
|
|
|
|
|
|
model: "모델",
|
|
|
|
|
|
year: "연식",
|
|
|
|
|
|
color: "색상",
|
|
|
|
|
|
type: "종류",
|
|
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 배송 관련
|
2025-10-22 13:40:15 +09:00
|
|
|
|
delivery_id: "배송ID",
|
|
|
|
|
|
order_id: "주문ID",
|
|
|
|
|
|
customer_name: "고객명",
|
|
|
|
|
|
address: "주소",
|
|
|
|
|
|
delivery_date: "배송일",
|
|
|
|
|
|
estimated_time: "예상시간",
|
|
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 제품 관련
|
2025-10-22 13:40:15 +09:00
|
|
|
|
product_id: "제품ID",
|
|
|
|
|
|
product_name: "제품명",
|
|
|
|
|
|
price: "가격",
|
|
|
|
|
|
stock: "재고",
|
|
|
|
|
|
category: "카테고리",
|
|
|
|
|
|
description: "설명",
|
|
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 주문 관련
|
2025-10-22 13:40:15 +09:00
|
|
|
|
order_date: "주문일",
|
|
|
|
|
|
quantity: "수량",
|
|
|
|
|
|
total_amount: "총금액",
|
|
|
|
|
|
payment_status: "결제상태",
|
|
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 고객 관련
|
2025-10-22 13:40:15 +09:00
|
|
|
|
customer_id: "고객ID",
|
|
|
|
|
|
email: "이메일",
|
|
|
|
|
|
company: "회사",
|
|
|
|
|
|
department: "부서",
|
2025-10-15 16:16:27 +09:00
|
|
|
|
};
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
columnTranslations[colName.toLowerCase()] || columnTranslations[colName.replace(/_/g, "").toLowerCase()] || colName
|
|
|
|
|
|
);
|
2025-10-15 16:16:27 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 범용 목록 위젯
|
|
|
|
|
|
* - 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();
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 자동 새로고침 (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);
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
const token = localStorage.getItem("authToken");
|
2025-10-24 16:08:57 +09:00
|
|
|
|
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
2025-10-15 16:16:27 +09:00
|
|
|
|
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();
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
if (result.success && result.data?.rows) {
|
|
|
|
|
|
const rows = result.data.rows;
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
// 컬럼 정보 추출 (한글 번역 적용)
|
|
|
|
|
|
if (rows.length > 0) {
|
|
|
|
|
|
const cols: ColumnInfo[] = Object.keys(rows[0]).map((key) => ({
|
|
|
|
|
|
key,
|
|
|
|
|
|
label: translateColumnName(key),
|
|
|
|
|
|
}));
|
|
|
|
|
|
setColumns(cols);
|
|
|
|
|
|
}
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
setData(rows);
|
|
|
|
|
|
}
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
2025-10-15 16:16:27 +09:00
|
|
|
|
setError(null);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 이름 한글 번역
|
|
|
|
|
|
const translateTableName = (name: string): string => {
|
|
|
|
|
|
const tableTranslations: { [key: string]: string } = {
|
2025-10-22 13:40:15 +09:00
|
|
|
|
drivers: "기사",
|
|
|
|
|
|
driver: "기사",
|
|
|
|
|
|
vehicles: "차량",
|
|
|
|
|
|
vehicle: "차량",
|
|
|
|
|
|
products: "제품",
|
|
|
|
|
|
product: "제품",
|
|
|
|
|
|
orders: "주문",
|
|
|
|
|
|
order: "주문",
|
|
|
|
|
|
customers: "고객",
|
|
|
|
|
|
customer: "고객",
|
|
|
|
|
|
deliveries: "배송",
|
|
|
|
|
|
delivery: "배송",
|
|
|
|
|
|
users: "사용자",
|
|
|
|
|
|
user: "사용자",
|
2025-10-15 16:16:27 +09:00
|
|
|
|
};
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
|
|
|
|
|
return tableTranslations[name.toLowerCase()] || tableTranslations[name.replace(/_/g, "").toLowerCase()] || name;
|
2025-10-15 16:16:27 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const displayTitle = tableName ? `${translateTableName(tableName)} 목록` : "데이터 목록";
|
|
|
|
|
|
|
|
|
|
|
|
// 검색 필터링
|
|
|
|
|
|
const filteredData = data.filter((row) =>
|
2025-10-22 13:40:15 +09:00
|
|
|
|
Object.values(row).some((value) => String(value).toLowerCase().includes(searchTerm.toLowerCase())),
|
2025-10-15 16:16:27 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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" />
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
2025-10-15 16:16:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="text-center text-destructive">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
<p className="text-sm">⚠️ {error}</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={loadData}
|
2025-10-29 17:53:03 +09:00
|
|
|
|
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
2025-10-15 16:16:27 +09:00
|
|
|
|
>
|
|
|
|
|
|
다시 시도
|
|
|
|
|
|
</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>
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<h3 className="text-sm font-bold text-foreground">데이터 목록</h3>
|
|
|
|
|
|
<div className="space-y-1.5 text-xs text-foreground">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
<p className="font-medium">📋 테이블 형식 데이터 표시 위젯</p>
|
|
|
|
|
|
<ul className="space-y-0.5 text-left">
|
|
|
|
|
|
<li>• SQL 쿼리로 데이터를 불러옵니다</li>
|
|
|
|
|
|
<li>• 테이블 형식으로 자동 표시</li>
|
|
|
|
|
|
<li>• 검색 기능 지원</li>
|
|
|
|
|
|
<li>• 실시간 데이터 모니터링 가능</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="mt-2 rounded-lg bg-primary/10 p-2 text-[10px] text-primary">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
<p>SQL 쿼리를 입력하고 저장하세요</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-background to-primary/10 p-2">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
|
|
|
|
|
<div className="flex-1">
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<h3 className="text-sm font-bold text-foreground">📋 {displayTitle}</h3>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">총 {filteredData.length.toLocaleString()}건</p>
|
2025-10-15 16:16:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={loadData}
|
2025-10-29 17:53:03 +09:00
|
|
|
|
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-background p-0 text-xs disabled:opacity-50"
|
2025-10-15 16:16:27 +09:00
|
|
|
|
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)}
|
2025-10-29 17:53:03 +09:00
|
|
|
|
className="focus:border-primary focus:ring-primary w-full rounded border border-border px-2 py-1 text-xs focus:ring-1 focus:outline-none"
|
2025-10-15 16:16:27 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 테이블 */}
|
|
|
|
|
|
<div className="flex-1 overflow-auto">
|
|
|
|
|
|
{filteredData.length > 0 ? (
|
|
|
|
|
|
<table className="w-full border-collapse text-xs">
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<thead className="sticky top-0 bg-muted">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
<tr>
|
|
|
|
|
|
{columns.map((col) => (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<th key={col.key} className="border border-border px-2 py-1 text-left font-semibold text-foreground">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
{col.label}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<tbody className="bg-background">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
{filteredData.map((row, idx) => (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<tr key={idx} className="hover:bg-muted">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
{columns.map((col) => (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<td key={col.key} className="border border-border px-2 py-1 text-foreground">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
{String(row[col.key] || "")}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
) : (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="flex h-full items-center justify-center text-muted-foreground">
|
2025-10-15 16:16:27 +09:00
|
|
|
|
<p className="text-sm">검색 결과가 없습니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|