"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card } from "@/components/ui/card"; import { Loader2, RefreshCw } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; import { getApiUrl } from "@/lib/utils/apiUrl"; interface ListTestWidgetProps { element: DashboardElement; } interface QueryResult { columns: string[]; rows: Record[]; totalRows: number; executionTime: number; } /** * 리스트 테스트 위젯 (다중 데이터 소스 지원) * - 여러 REST API 연결 가능 * - 여러 Database 연결 가능 * - REST API + Database 혼합 가능 * - 데이터 자동 병합 */ export function ListTestWidget({ element }: ListTestWidgetProps) { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [lastRefreshTime, setLastRefreshTime] = useState(null); // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; }, [element?.dataSources, element?.chartConfig?.dataSources]); // // console.log("📊 dataSources 확인:", { // hasDataSources: !!dataSources, // dataSourcesLength: dataSources?.length || 0, // dataSources: dataSources, // }); const config = element.listConfig || { columnMode: "auto", viewMode: "table", columns: [], pageSize: 10, enablePagination: true, showHeader: true, stripedRows: true, compactMode: false, cardColumns: 3, }; // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { if (!dataSources || dataSources.length === 0) { // console.log("⚠️ 데이터 소스가 없습니다."); return; } // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setIsLoading(true); setError(null); try { // 모든 데이터 소스를 병렬로 로딩 const results = await Promise.allSettled( dataSources.map(async (source) => { try { // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); } else if (source.type === "database") { return await loadDatabaseData(source); } return { columns: [], rows: [] }; } catch (err: any) { console.error(`❌ 데이터 소스 "${source.name || source.id}" 로딩 실패:`, err); return { columns: [], rows: [] }; } }) ); // 성공한 데이터만 병합 const allColumns = new Set(); const allRows: Record[] = []; results.forEach((result, index) => { if (result.status === "fulfilled") { const { columns, rows } = result.value; // 컬럼 수집 columns.forEach((col: string) => allColumns.add(col)); // 행 병합 (소스 정보 추가) const sourceName = dataSources[index].name || dataSources[index].id || `소스 ${index + 1}`; rows.forEach((row: any) => { allRows.push({ ...row, _source: sourceName, }); }); } }); const finalColumns = Array.from(allColumns); // _source 컬럼을 맨 앞으로 const sortedColumns = finalColumns.includes("_source") ? ["_source", ...finalColumns.filter((c) => c !== "_source")] : finalColumns; setData({ columns: sortedColumns, rows: allRows, totalRows: allRows.length, executionTime: 0, }); setLastRefreshTime(new Date()); // console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`); } catch (err) { setError(err instanceof Error ? err.message : "데이터 로딩 실패"); } finally { setIsLoading(false); } }, [dataSources]); // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); // REST API 데이터 로딩 const loadRestApiData = async (source: ChartDataSource): Promise<{ columns: string[]; rows: any[] }> => { if (!source.endpoint) { throw new Error("API endpoint가 없습니다."); } const params = new URLSearchParams(); if (source.queryParams) { Object.entries(source.queryParams).forEach(([key, value]) => { if (key && value) { params.append(key, String(value)); } }); } const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", body: JSON.stringify({ url: source.endpoint, method: "GET", headers: source.headers || {}, queryParams: Object.fromEntries(params), }), }); if (!response.ok) { const errorText = await response.text(); console.error("❌ API 호출 실패:", { status: response.status, statusText: response.statusText, body: errorText.substring(0, 500), }); throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); // console.log("✅ API 응답:", result); if (!result.success) { console.error("❌ API 실패:", result); throw new Error(result.message || result.error || "외부 API 호출 실패"); } let processedData = result.data; // JSON Path 처리 if (source.jsonPath) { const paths = source.jsonPath.split("."); for (const path of paths) { if (processedData && typeof processedData === "object" && path in processedData) { processedData = processedData[path]; } else { throw new Error(`JSON Path "${source.jsonPath}"에서 데이터를 찾을 수 없습니다`); } } } let rows = Array.isArray(processedData) ? processedData : [processedData]; // 컬럼 매핑 적용 rows = applyColumnMapping(rows, source.columnMapping); const columns = rows.length > 0 ? Object.keys(rows[0]) : []; return { columns, rows }; }; // Database 데이터 로딩 const loadDatabaseData = async (source: ChartDataSource): Promise<{ columns: string[]; rows: any[] }> => { if (!source.query) { throw new Error("SQL 쿼리가 없습니다."); } if (source.connectionType === "external" && source.externalConnectionId) { // 외부 DB const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); const externalResult = await ExternalDbConnectionAPI.executeQuery( parseInt(source.externalConnectionId), source.query, ); if (!externalResult.success || !externalResult.data) { throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패"); } const resultData = externalResult.data as unknown as { columns: string[]; rows: Record[]; }; // 컬럼 매핑 적용 const mappedRows = applyColumnMapping(resultData.rows, source.columnMapping); const columns = mappedRows.length > 0 ? Object.keys(mappedRows[0]) : resultData.columns; return { columns, rows: mappedRows, }; } else { // 현재 DB const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(source.query); // // console.log("💾 내부 DB 쿼리 결과:", { // hasRows: !!result.rows, // rowCount: result.rows?.length || 0, // hasColumns: !!result.columns, // columnCount: result.columns?.length || 0, // firstRow: result.rows?.[0], // resultKeys: Object.keys(result), // }); // 컬럼 매핑 적용 const mappedRows = applyColumnMapping(result.rows, source.columnMapping); const columns = mappedRows.length > 0 ? Object.keys(mappedRows[0]) : result.columns; // // console.log("✅ 매핑 후:", { // columns, // rowCount: mappedRows.length, // firstMappedRow: mappedRows[0], // }); return { columns, rows: mappedRows, }; } }; // 초기 로드 useEffect(() => { if (dataSources && dataSources.length > 0) { loadMultipleDataSources(); } }, [dataSources, loadMultipleDataSources]); // 자동 새로고침 useEffect(() => { if (!dataSources || dataSources.length === 0) return; const intervals = dataSources .map((ds) => ds.refreshInterval) .filter((interval): interval is number => typeof interval === "number" && interval > 0); if (intervals.length === 0) return; const minInterval = Math.min(...intervals); // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); // 페이지네이션 const pageSize = config.pageSize || 10; const totalPages = data ? Math.ceil(data.totalRows / pageSize) : 0; const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedRows = data?.rows.slice(startIndex, endIndex) || []; // 테이블 뷰 const renderTable = () => (
{config.showHeader && ( {data?.columns.map((col) => ( {col} ))} )} {paginatedRows.map((row, idx) => ( {data?.columns.map((col) => ( {String(row[col] ?? "")} ))} ))}
); // 카드 뷰 const renderCards = () => (
{paginatedRows.map((row, idx) => ( {data?.columns.map((col) => (
{col}: {String(row[col] ?? "")}
))}
))}
); return (
{/* 헤더 */}

{element?.customTitle || "리스트"}

{dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행 {lastRefreshTime && ( • {lastRefreshTime.toLocaleTimeString("ko-KR")} )}

{isLoading && }
{/* 컨텐츠 */}
{error ? (

{error}

) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (

데이터 소스를 연결해주세요

) : !data || data.rows.length === 0 ? (

데이터가 없습니다

) : config.viewMode === "card" ? ( renderCards() ) : ( renderTable() )}
{/* 페이지네이션 */} {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && (
총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages})
)}
); }