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}
}
-
+
{/* 새로고침 버튼 */}
🔄
-
+
{/* 전체화면 버튼 */}
{
@@ -134,26 +126,26 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
document.documentElement.requestFullscreen();
}
}}
- className="px-3 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg hover:bg-gray-50"
+ className="rounded-lg border border-gray-300 px-3 py-2 text-gray-600 hover:bg-gray-50 hover:text-gray-800"
title="전체화면"
>
⛶
-
+
{/* 편집 버튼 */}
{
- window.open(`/admin/dashboard?load=${params.dashboardId}`, '_blank');
+ window.open(`/admin/dashboard?load=${resolvedParams.dashboardId}`, "_blank");
}}
- className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
+ className="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
편집
-
+
{/* 메타 정보 */}
-
+
생성: {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 && (
{isLoading ? (
-
+
) : (
- '🔄'
+ "🔄"
)}
)}
@@ -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) },
];
}