diff --git a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx index f01e31ad..3084bfc1 100644 --- a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx +++ b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx @@ -1,13 +1,13 @@ -'use client'; +"use client"; -import React, { useState, useEffect } from 'react'; -import { DashboardViewer } from '@/components/dashboard/DashboardViewer'; -import { DashboardElement } from '@/components/admin/dashboard/types'; +import React, { useState, useEffect, use } from "react"; +import { DashboardViewer } from "@/components/dashboard/DashboardViewer"; +import { DashboardElement } from "@/components/admin/dashboard/types"; interface DashboardViewPageProps { - params: { + params: Promise<{ dashboardId: string; - }; + }>; } /** @@ -17,6 +17,7 @@ interface DashboardViewPageProps { * - 전체화면 모드 지원 */ export default function DashboardViewPage({ params }: DashboardViewPageProps) { + const resolvedParams = use(params); const [dashboard, setDashboard] = useState<{ id: string; title: string; @@ -31,7 +32,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { // 대시보드 데이터 로딩 useEffect(() => { loadDashboard(); - }, [params.dashboardId]); + }, [resolvedParams.dashboardId]); const loadDashboard = async () => { setIsLoading(true); @@ -39,29 +40,29 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { try { // 실제 API 호출 시도 - const { dashboardApi } = await import('@/lib/api/dashboard'); - + const { dashboardApi } = await import("@/lib/api/dashboard"); + try { - const dashboardData = await dashboardApi.getDashboard(params.dashboardId); + const dashboardData = await dashboardApi.getDashboard(resolvedParams.dashboardId); setDashboard(dashboardData); } catch (apiError) { - console.warn('API 호출 실패, 로컬 스토리지 확인:', apiError); - + console.warn("API 호출 실패, 로컬 스토리지 확인:", apiError); + // API 실패 시 로컬 스토리지에서 찾기 - const savedDashboards = JSON.parse(localStorage.getItem('savedDashboards') || '[]'); - const savedDashboard = savedDashboards.find((d: any) => d.id === params.dashboardId); - + const savedDashboards = JSON.parse(localStorage.getItem("savedDashboards") || "[]"); + const savedDashboard = savedDashboards.find((d: any) => d.id === resolvedParams.dashboardId); + if (savedDashboard) { setDashboard(savedDashboard); } else { // 로컬에도 없으면 샘플 데이터 사용 - const sampleDashboard = generateSampleDashboard(params.dashboardId); + const sampleDashboard = generateSampleDashboard(resolvedParams.dashboardId); setDashboard(sampleDashboard); } } } catch (err) { - setError('대시보드를 불러오는 중 오류가 발생했습니다.'); - console.error('Dashboard loading error:', err); + setError("대시보드를 불러오는 중 오류가 발생했습니다."); + console.error("Dashboard loading error:", err); } finally { setIsLoading(false); } @@ -70,11 +71,11 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { // 로딩 상태 if (isLoading) { return ( -
+
-
+
대시보드 로딩 중...
-
잠시만 기다려주세요
+
잠시만 기다려주세요
); @@ -83,19 +84,12 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { // 에러 상태 if (error || !dashboard) { return ( -
+
-
😞
-
- {error || '대시보드를 찾을 수 없습니다'} -
-
- 대시보드 ID: {params.dashboardId} -
-
@@ -106,25 +100,23 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { return (
{/* 대시보드 헤더 */} -
-
+
+

{dashboard.title}

- {dashboard.description && ( -

{dashboard.description}

- )} + {dashboard.description &&

{dashboard.description}

}
- +
{/* 새로고침 버튼 */} - + {/* 전체화면 버튼 */} - + {/* 편집 버튼 */}
- + {/* 메타 정보 */} -
+
생성: {new Date(dashboard.createdAt).toLocaleString()} 수정: {new Date(dashboard.updatedAt).toLocaleString()} 요소: {dashboard.elements.length}개 @@ -162,10 +154,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { {/* 대시보드 뷰어 */}
- +
); @@ -176,111 +165,113 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { */ function generateSampleDashboard(dashboardId: string) { const dashboards: Record = { - 'sales-overview': { - id: 'sales-overview', - title: '📊 매출 현황 대시보드', - description: '월별 매출 추이 및 상품별 판매 현황을 한눈에 확인할 수 있습니다.', + "sales-overview": { + id: "sales-overview", + title: "📊 매출 현황 대시보드", + description: "월별 매출 추이 및 상품별 판매 현황을 한눈에 확인할 수 있습니다.", elements: [ { - id: 'chart-1', - type: 'chart', - subtype: 'bar', + id: "chart-1", + type: "chart", + subtype: "bar", position: { x: 20, y: 20 }, size: { width: 400, height: 300 }, - title: '📊 월별 매출 추이', - content: '월별 매출 데이터', + title: "📊 월별 매출 추이", + content: "월별 매출 데이터", dataSource: { - type: 'database', - query: 'SELECT month, sales FROM monthly_sales', - refreshInterval: 30000 + type: "database", + query: "SELECT month, sales FROM monthly_sales", + refreshInterval: 30000, }, chartConfig: { - xAxis: 'month', - yAxis: 'sales', - title: '월별 매출 추이', - colors: ['#3B82F6', '#EF4444', '#10B981'] - } + xAxis: "month", + yAxis: "sales", + title: "월별 매출 추이", + colors: ["#3B82F6", "#EF4444", "#10B981"], + }, }, { - id: 'chart-2', - type: 'chart', - subtype: 'pie', + id: "chart-2", + type: "chart", + subtype: "pie", position: { x: 450, y: 20 }, size: { width: 350, height: 300 }, - title: '🥧 상품별 판매 비율', - content: '상품별 판매 데이터', + title: "🥧 상품별 판매 비율", + content: "상품별 판매 데이터", dataSource: { - type: 'database', - query: 'SELECT product_name, total_sold FROM product_sales', - refreshInterval: 60000 + type: "database", + query: "SELECT product_name, total_sold FROM product_sales", + refreshInterval: 60000, }, chartConfig: { - xAxis: 'product_name', - yAxis: 'total_sold', - title: '상품별 판매 비율', - colors: ['#8B5CF6', '#EC4899', '#06B6D4', '#84CC16'] - } + xAxis: "product_name", + yAxis: "total_sold", + title: "상품별 판매 비율", + colors: ["#8B5CF6", "#EC4899", "#06B6D4", "#84CC16"], + }, }, { - id: 'chart-3', - type: 'chart', - subtype: 'line', + id: "chart-3", + type: "chart", + subtype: "line", position: { x: 20, y: 350 }, size: { width: 780, height: 250 }, - title: '📈 사용자 가입 추이', - content: '사용자 가입 데이터', + title: "📈 사용자 가입 추이", + content: "사용자 가입 데이터", dataSource: { - type: 'database', - query: 'SELECT week, new_users FROM user_growth', - refreshInterval: 300000 + type: "database", + query: "SELECT week, new_users FROM user_growth", + refreshInterval: 300000, }, chartConfig: { - xAxis: 'week', - yAxis: 'new_users', - title: '주간 신규 사용자 가입 추이', - colors: ['#10B981'] - } - } + xAxis: "week", + yAxis: "new_users", + title: "주간 신규 사용자 가입 추이", + colors: ["#10B981"], + }, + }, ], - createdAt: '2024-09-30T10:00:00Z', - updatedAt: '2024-09-30T14:30:00Z' + createdAt: "2024-09-30T10:00:00Z", + updatedAt: "2024-09-30T14:30:00Z", }, - 'user-analytics': { - id: 'user-analytics', - title: '👥 사용자 분석 대시보드', - description: '사용자 행동 패턴 및 가입 추이 분석', + "user-analytics": { + id: "user-analytics", + title: "👥 사용자 분석 대시보드", + description: "사용자 행동 패턴 및 가입 추이 분석", elements: [ { - id: 'chart-4', - type: 'chart', - subtype: 'line', + id: "chart-4", + type: "chart", + subtype: "line", position: { x: 20, y: 20 }, size: { width: 500, height: 300 }, - title: '📈 일일 활성 사용자', - content: '사용자 활동 데이터', + title: "📈 일일 활성 사용자", + content: "사용자 활동 데이터", dataSource: { - type: 'database', - query: 'SELECT date, active_users FROM daily_active_users', - refreshInterval: 60000 + type: "database", + query: "SELECT date, active_users FROM daily_active_users", + refreshInterval: 60000, }, chartConfig: { - xAxis: 'date', - yAxis: 'active_users', - title: '일일 활성 사용자 추이' - } - } + xAxis: "date", + yAxis: "active_users", + title: "일일 활성 사용자 추이", + }, + }, ], - createdAt: '2024-09-29T15:00:00Z', - updatedAt: '2024-09-30T09:15:00Z' - } + createdAt: "2024-09-29T15:00:00Z", + updatedAt: "2024-09-30T09:15:00Z", + }, }; - return dashboards[dashboardId] || { - id: dashboardId, - title: `대시보드 ${dashboardId}`, - description: '샘플 대시보드입니다.', - elements: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; + return ( + dashboards[dashboardId] || { + id: dashboardId, + title: `대시보드 ${dashboardId}`, + description: "샘플 대시보드입니다.", + elements: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + ); } diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index da75a511..b8517d73 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import React, { useState, useEffect, useCallback } from 'react'; -import { DashboardElement, QueryResult } from '@/components/admin/dashboard/types'; -import { ChartRenderer } from '@/components/admin/dashboard/charts/ChartRenderer'; +import React, { useState, useEffect, useCallback } from "react"; +import { DashboardElement, QueryResult } from "@/components/admin/dashboard/types"; +import { ChartRenderer } from "@/components/admin/dashboard/charts/ChartRenderer"; interface DashboardViewerProps { elements: DashboardElement[]; @@ -23,36 +23,60 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash // 개별 요소 데이터 로딩 const loadElementData = useCallback(async (element: DashboardElement) => { - if (!element.dataSource?.query || element.type !== 'chart') { + if (!element.dataSource?.query || element.type !== "chart") { return; } - setLoadingElements(prev => new Set([...prev, element.id])); + setLoadingElements((prev) => new Set([...prev, element.id])); try { - // console.log(`🔄 요소 ${element.id} 데이터 로딩 시작:`, element.dataSource.query); - - // 실제 API 호출 - const { dashboardApi } = await import('@/lib/api/dashboard'); - const result = await dashboardApi.executeQuery(element.dataSource.query); - - // console.log(`✅ 요소 ${element.id} 데이터 로딩 완료:`, result); - - const data: QueryResult = { - columns: result.columns || [], - rows: result.rows || [], - totalRows: result.rowCount || 0, - executionTime: 0 - }; - - setElementData(prev => ({ - ...prev, - [element.id]: data - })); + let result; + + // 외부 DB vs 현재 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 쿼리 실행 실패"); + } + + const data: QueryResult = { + columns: externalResult.data?.[0] ? Object.keys(externalResult.data[0]) : [], + rows: externalResult.data || [], + totalRows: externalResult.data?.length || 0, + executionTime: 0, + }; + + setElementData((prev) => ({ + ...prev, + [element.id]: data, + })); + } else { + // 현재 DB + const { dashboardApi } = await import("@/lib/api/dashboard"); + result = await dashboardApi.executeQuery(element.dataSource.query); + + const data: QueryResult = { + columns: result.columns || [], + rows: result.rows || [], + totalRows: result.rowCount || 0, + executionTime: 0, + }; + + setElementData((prev) => ({ + ...prev, + [element.id]: data, + })); + } } catch (error) { - // console.error(`❌ Element ${element.id} data loading error:`, error); + // 에러 발생 시 무시 (차트는 빈 상태로 표시됨) } finally { - setLoadingElements(prev => { + setLoadingElements((prev) => { const newSet = new Set(prev); newSet.delete(element.id); return newSet; @@ -63,11 +87,11 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash // 모든 요소 데이터 로딩 const loadAllData = useCallback(async () => { setLastRefresh(new Date()); - - const chartElements = elements.filter(el => el.type === 'chart' && el.dataSource?.query); - + + const chartElements = elements.filter((el) => el.type === "chart" && el.dataSource?.query); + // 병렬로 모든 차트 데이터 로딩 - await Promise.all(chartElements.map(element => loadElementData(element))); + await Promise.all(chartElements.map((element) => loadElementData(element))); }, [elements, loadElementData]); // 초기 데이터 로딩 @@ -88,34 +112,28 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash // 요소가 없는 경우 if (elements.length === 0) { return ( -
+
-
📊
-
- 표시할 요소가 없습니다 -
-
- 대시보드 편집기에서 차트나 위젯을 추가해보세요 -
+
📊
+
표시할 요소가 없습니다
+
대시보드 편집기에서 차트나 위젯을 추가해보세요
); } return ( -
+
{/* 새로고침 상태 표시 */} -
+
마지막 업데이트: {lastRefresh.toLocaleTimeString()} {Array.from(loadingElements).length > 0 && ( - - ({Array.from(loadingElements).length}개 로딩 중...) - + ({Array.from(loadingElements).length}개 로딩 중...) )}
{/* 대시보드 요소들 */} -
+
{elements.map((element) => ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* 헤더 */} -
-

{element.title}

- +
+

{element.title}

+ {/* 새로고침 버튼 (호버 시에만 표시) */} {isHovered && ( )} @@ -178,20 +196,15 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro {/* 내용 */}
- {element.type === 'chart' ? ( - + {element.type === "chart" ? ( + ) : ( // 위젯 렌더링 -
+
-
- {element.subtype === 'exchange' && '💱'} - {element.subtype === 'weather' && '☁️'} +
+ {element.subtype === "exchange" && "💱"} + {element.subtype === "weather" && "☁️"}
{element.content}
@@ -201,10 +214,10 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro {/* 로딩 오버레이 */} {isLoading && ( -
+
-
-
업데이트 중...
+
+
업데이트 중...
)} @@ -218,53 +231,73 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro function generateSampleQueryResult(query: string, chartType: string): QueryResult { // 시간에 따라 약간씩 다른 데이터 생성 (실시간 업데이트 시뮬레이션) const timeVariation = Math.sin(Date.now() / 10000) * 0.1 + 1; - - const isMonthly = query.toLowerCase().includes('month'); - const isSales = query.toLowerCase().includes('sales') || query.toLowerCase().includes('매출'); - const isUsers = query.toLowerCase().includes('users') || query.toLowerCase().includes('사용자'); - const isProducts = query.toLowerCase().includes('product') || query.toLowerCase().includes('상품'); - const isWeekly = query.toLowerCase().includes('week'); + + const isMonthly = query.toLowerCase().includes("month"); + const isSales = query.toLowerCase().includes("sales") || query.toLowerCase().includes("매출"); + const isUsers = query.toLowerCase().includes("users") || query.toLowerCase().includes("사용자"); + const isProducts = query.toLowerCase().includes("product") || query.toLowerCase().includes("상품"); + const isWeekly = query.toLowerCase().includes("week"); let columns: string[]; let rows: Record[]; if (isMonthly && isSales) { - columns = ['month', 'sales', 'order_count']; + columns = ["month", "sales", "order_count"]; rows = [ - { month: '2024-01', sales: Math.round(1200000 * timeVariation), order_count: Math.round(45 * timeVariation) }, - { month: '2024-02', sales: Math.round(1350000 * timeVariation), order_count: Math.round(52 * timeVariation) }, - { month: '2024-03', sales: Math.round(1180000 * timeVariation), order_count: Math.round(41 * timeVariation) }, - { month: '2024-04', sales: Math.round(1420000 * timeVariation), order_count: Math.round(58 * timeVariation) }, - { month: '2024-05', sales: Math.round(1680000 * timeVariation), order_count: Math.round(67 * timeVariation) }, - { month: '2024-06', sales: Math.round(1540000 * timeVariation), order_count: Math.round(61 * timeVariation) }, + { month: "2024-01", sales: Math.round(1200000 * timeVariation), order_count: Math.round(45 * timeVariation) }, + { month: "2024-02", sales: Math.round(1350000 * timeVariation), order_count: Math.round(52 * timeVariation) }, + { month: "2024-03", sales: Math.round(1180000 * timeVariation), order_count: Math.round(41 * timeVariation) }, + { month: "2024-04", sales: Math.round(1420000 * timeVariation), order_count: Math.round(58 * timeVariation) }, + { month: "2024-05", sales: Math.round(1680000 * timeVariation), order_count: Math.round(67 * timeVariation) }, + { month: "2024-06", sales: Math.round(1540000 * timeVariation), order_count: Math.round(61 * timeVariation) }, ]; } else if (isWeekly && isUsers) { - columns = ['week', 'new_users']; + columns = ["week", "new_users"]; rows = [ - { week: '2024-W10', new_users: Math.round(23 * timeVariation) }, - { week: '2024-W11', new_users: Math.round(31 * timeVariation) }, - { week: '2024-W12', new_users: Math.round(28 * timeVariation) }, - { week: '2024-W13', new_users: Math.round(35 * timeVariation) }, - { week: '2024-W14', new_users: Math.round(42 * timeVariation) }, - { week: '2024-W15', new_users: Math.round(38 * timeVariation) }, + { week: "2024-W10", new_users: Math.round(23 * timeVariation) }, + { week: "2024-W11", new_users: Math.round(31 * timeVariation) }, + { week: "2024-W12", new_users: Math.round(28 * timeVariation) }, + { week: "2024-W13", new_users: Math.round(35 * timeVariation) }, + { week: "2024-W14", new_users: Math.round(42 * timeVariation) }, + { week: "2024-W15", new_users: Math.round(38 * timeVariation) }, ]; } else if (isProducts) { - columns = ['product_name', 'total_sold', 'revenue']; + columns = ["product_name", "total_sold", "revenue"]; rows = [ - { product_name: '스마트폰', total_sold: Math.round(156 * timeVariation), revenue: Math.round(234000000 * timeVariation) }, - { product_name: '노트북', total_sold: Math.round(89 * timeVariation), revenue: Math.round(178000000 * timeVariation) }, - { product_name: '태블릿', total_sold: Math.round(134 * timeVariation), revenue: Math.round(67000000 * timeVariation) }, - { product_name: '이어폰', total_sold: Math.round(267 * timeVariation), revenue: Math.round(26700000 * timeVariation) }, - { product_name: '스마트워치', total_sold: Math.round(98 * timeVariation), revenue: Math.round(49000000 * timeVariation) }, + { + product_name: "스마트폰", + total_sold: Math.round(156 * timeVariation), + revenue: Math.round(234000000 * timeVariation), + }, + { + product_name: "노트북", + total_sold: Math.round(89 * timeVariation), + revenue: Math.round(178000000 * timeVariation), + }, + { + product_name: "태블릿", + total_sold: Math.round(134 * timeVariation), + revenue: Math.round(67000000 * timeVariation), + }, + { + product_name: "이어폰", + total_sold: Math.round(267 * timeVariation), + revenue: Math.round(26700000 * timeVariation), + }, + { + product_name: "스마트워치", + total_sold: Math.round(98 * timeVariation), + revenue: Math.round(49000000 * timeVariation), + }, ]; } else { - columns = ['category', 'value', 'count']; + columns = ["category", "value", "count"]; rows = [ - { category: 'A', value: Math.round(100 * timeVariation), count: Math.round(10 * timeVariation) }, - { category: 'B', value: Math.round(150 * timeVariation), count: Math.round(15 * timeVariation) }, - { category: 'C', value: Math.round(120 * timeVariation), count: Math.round(12 * timeVariation) }, - { category: 'D', value: Math.round(180 * timeVariation), count: Math.round(18 * timeVariation) }, - { category: 'E', value: Math.round(90 * timeVariation), count: Math.round(9 * timeVariation) }, + { category: "A", value: Math.round(100 * timeVariation), count: Math.round(10 * timeVariation) }, + { category: "B", value: Math.round(150 * timeVariation), count: Math.round(15 * timeVariation) }, + { category: "C", value: Math.round(120 * timeVariation), count: Math.round(12 * timeVariation) }, + { category: "D", value: Math.round(180 * timeVariation), count: Math.round(18 * timeVariation) }, + { category: "E", value: Math.round(90 * timeVariation), count: Math.round(9 * timeVariation) }, ]; }