에러 해결
This commit is contained in:
@@ -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 (
|
||||
<div className="h-full flex items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4">📊</div>
|
||||
<div className="text-xl font-medium text-gray-700 mb-2">
|
||||
표시할 요소가 없습니다
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
대시보드 편집기에서 차트나 위젯을 추가해보세요
|
||||
</div>
|
||||
<div className="mb-4 text-6xl">📊</div>
|
||||
<div className="mb-2 text-xl font-medium text-gray-700">표시할 요소가 없습니다</div>
|
||||
<div className="text-sm text-gray-500">대시보드 편집기에서 차트나 위젯을 추가해보세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gray-100 overflow-auto">
|
||||
<div className="relative h-full w-full overflow-auto bg-gray-100">
|
||||
{/* 새로고침 상태 표시 */}
|
||||
<div className="absolute top-4 right-4 z-10 bg-white rounded-lg shadow-sm px-3 py-2 text-xs text-muted-foreground">
|
||||
<div className="text-muted-foreground absolute top-4 right-4 z-10 rounded-lg bg-white px-3 py-2 text-xs shadow-sm">
|
||||
마지막 업데이트: {lastRefresh.toLocaleTimeString()}
|
||||
{Array.from(loadingElements).length > 0 && (
|
||||
<span className="ml-2 text-primary">
|
||||
({Array.from(loadingElements).length}개 로딩 중...)
|
||||
</span>
|
||||
<span className="text-primary ml-2">({Array.from(loadingElements).length}개 로딩 중...)</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 대시보드 요소들 */}
|
||||
<div className="relative" style={{ minHeight: '100%' }}>
|
||||
<div className="relative" style={{ minHeight: "100%" }}>
|
||||
{elements.map((element) => (
|
||||
<ViewerElement
|
||||
key={element.id}
|
||||
@@ -145,32 +163,32 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
|
||||
className="absolute overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
style={{
|
||||
left: element.position.x,
|
||||
top: element.position.y,
|
||||
width: element.size.width,
|
||||
height: element.size.height
|
||||
height: element.size.height,
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-gray-800 text-sm">{element.title}</h3>
|
||||
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<h3 className="text-sm font-semibold text-gray-800">{element.title}</h3>
|
||||
|
||||
{/* 새로고침 버튼 (호버 시에만 표시) */}
|
||||
{isHovered && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 hover:text-muted-foreground disabled:opacity-50"
|
||||
className="hover:text-muted-foreground text-gray-400 disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-4 h-4 border border-gray-400 border-t-transparent rounded-full animate-spin" />
|
||||
<div className="h-4 w-4 animate-spin rounded-full border border-gray-400 border-t-transparent" />
|
||||
) : (
|
||||
'🔄'
|
||||
"🔄"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -178,20 +196,15 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="h-[calc(100%-57px)]">
|
||||
{element.type === 'chart' ? (
|
||||
<ChartRenderer
|
||||
element={element}
|
||||
data={data}
|
||||
width={element.size.width}
|
||||
height={element.size.height - 57}
|
||||
/>
|
||||
{element.type === "chart" ? (
|
||||
<ChartRenderer element={element} data={data} width={element.size.width} height={element.size.height - 57} />
|
||||
) : (
|
||||
// 위젯 렌더링
|
||||
<div className="w-full h-full p-4 flex items-center justify-center bg-gradient-to-br from-blue-400 to-purple-600 text-white">
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-blue-400 to-purple-600 p-4 text-white">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2">
|
||||
{element.subtype === 'exchange' && '💱'}
|
||||
{element.subtype === 'weather' && '☁️'}
|
||||
<div className="mb-2 text-3xl">
|
||||
{element.subtype === "exchange" && "💱"}
|
||||
{element.subtype === "weather" && "☁️"}
|
||||
</div>
|
||||
<div className="text-sm whitespace-pre-line">{element.content}</div>
|
||||
</div>
|
||||
@@ -201,10 +214,10 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
||||
|
||||
{/* 로딩 오버레이 */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<div className="text-sm text-muted-foreground">업데이트 중...</div>
|
||||
<div className="border-primary mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<div className="text-muted-foreground text-sm">업데이트 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -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<string, any>[];
|
||||
|
||||
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) },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user