"use client"; import React, { useState, useEffect } from "react"; import { DashboardElement, QueryResult, ListWidgetConfig } from "../types"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card } from "@/components/ui/card"; interface ListWidgetProps { element: DashboardElement; onConfigUpdate?: (config: Partial) => void; } /** * 리스트 위젯 컴포넌트 * - DB 쿼리 또는 REST API로 데이터 가져오기 * - 테이블 형태로 데이터 표시 * - 페이지네이션, 정렬, 검색 기능 */ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const config = element.listConfig || { columnMode: "auto", viewMode: "table", columns: [], pageSize: 10, enablePagination: true, showHeader: true, stripedRows: true, compactMode: false, cardColumns: 3, }; // 데이터 로드 useEffect(() => { const loadData = async () => { if (!element.dataSource || (!element.dataSource.query && !element.dataSource.endpoint)) return; setIsLoading(true); setError(null); try { let queryResult: QueryResult; // REST API vs Database 분기 if (element.dataSource.type === "api" && element.dataSource.endpoint) { // REST API - 백엔드 프록시를 통한 호출 const params = new URLSearchParams(); if (element.dataSource.queryParams) { Object.entries(element.dataSource.queryParams).forEach(([key, value]) => { if (key && value) { params.append(key, value); } }); } const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: element.dataSource.endpoint, method: "GET", headers: element.dataSource.headers || {}, queryParams: Object.fromEntries(params), }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.message || "외부 API 호출 실패"); } const apiData = result.data; // JSON Path 처리 let processedData = apiData; if (element.dataSource.jsonPath) { const paths = element.dataSource.jsonPath.split("."); for (const path of paths) { if (processedData && typeof processedData === "object" && path in processedData) { processedData = processedData[path]; } else { throw new Error(`JSON Path "${element.dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다`); } } } const rows = Array.isArray(processedData) ? processedData : [processedData]; const columns = rows.length > 0 ? Object.keys(rows[0]) : []; queryResult = { columns, rows, totalRows: rows.length, executionTime: 0, }; } else if (element.dataSource.query) { // Database (현재 DB 또는 외부 DB) if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) { // 외부 DB const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); const externalResult = await ExternalDbConnectionAPI.executeQuery( parseInt(element.dataSource.externalConnectionId), element.dataSource.query, ); if (!externalResult.success) { throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패"); } queryResult = { columns: externalResult.data.columns, rows: externalResult.data.rows, totalRows: externalResult.data.rowCount, executionTime: 0, }; } else { // 현재 DB const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(element.dataSource.query); queryResult = { columns: result.columns, rows: result.rows, totalRows: result.rowCount, executionTime: 0, }; } } else { throw new Error("데이터 소스가 올바르게 설정되지 않았습니다"); } setData(queryResult); } catch (err) { setError(err instanceof Error ? err.message : "데이터 로딩 실패"); } finally { setIsLoading(false); } }; loadData(); // 자동 새로고침 설정 const refreshInterval = element.dataSource?.refreshInterval; if (refreshInterval && refreshInterval > 0) { const interval = setInterval(loadData, refreshInterval); return () => clearInterval(interval); } }, [ element.dataSource?.query, element.dataSource?.connectionType, element.dataSource?.externalConnectionId, element.dataSource?.endpoint, element.dataSource?.refreshInterval, ]); // 로딩 중 if (isLoading) { return (
데이터 로딩 중...
); } // 에러 if (error) { return (
⚠️
오류 발생
{error}
); } // 데이터 없음 if (!data) { return (
📋
리스트를 설정하세요
⚙️ 버튼을 클릭하여 데이터 소스와 컬럼을 설정해주세요
); } // 컬럼 설정이 없으면 자동으로 모든 컬럼 표시 const displayColumns = config.columns.length > 0 ? config.columns : data.columns.map((col) => ({ id: col, name: col, dataKey: col, visible: true, })); // 페이지네이션 const totalPages = Math.ceil(data.rows.length / config.pageSize); const startIdx = (currentPage - 1) * config.pageSize; const endIdx = startIdx + config.pageSize; const paginatedRows = config.enablePagination ? data.rows.slice(startIdx, endIdx) : data.rows; return (

{element.title}

{/* 테이블 뷰 */} {config.viewMode === "table" && (
{config.showHeader && ( {displayColumns .filter((col) => col.visible) .map((col) => ( {col.label || col.name} ))} )} {paginatedRows.length === 0 ? ( col.visible).length} className="text-center text-gray-500" > 데이터가 없습니다 ) : ( paginatedRows.map((row, idx) => ( {displayColumns .filter((col) => col.visible) .map((col) => ( {String(row[col.dataKey || col.field] ?? "")} ))} )) )}
)} {/* 카드 뷰 */} {config.viewMode === "card" && (
{paginatedRows.length === 0 ? (
데이터가 없습니다
) : (
{paginatedRows.map((row, idx) => (
{displayColumns .filter((col) => col.visible) .map((col) => (
{col.label || col.name}
{String(row[col.dataKey || col.field] ?? "")}
))}
))}
)}
)} {/* 페이지네이션 */} {config.enablePagination && totalPages > 1 && (
{startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개
{currentPage} / {totalPages}
)}
); }