에러 해결

This commit is contained in:
dohyeons 2025-10-14 17:09:07 +09:00
parent 2d57c5e9ee
commit dae3f2d4a8
2 changed files with 248 additions and 224 deletions

View File

@ -1,13 +1,13 @@
'use client'; "use client";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, use } from "react";
import { DashboardViewer } from '@/components/dashboard/DashboardViewer'; import { DashboardViewer } from "@/components/dashboard/DashboardViewer";
import { DashboardElement } from '@/components/admin/dashboard/types'; import { DashboardElement } from "@/components/admin/dashboard/types";
interface DashboardViewPageProps { interface DashboardViewPageProps {
params: { params: Promise<{
dashboardId: string; dashboardId: string;
}; }>;
} }
/** /**
@ -17,6 +17,7 @@ interface DashboardViewPageProps {
* - * -
*/ */
export default function DashboardViewPage({ params }: DashboardViewPageProps) { export default function DashboardViewPage({ params }: DashboardViewPageProps) {
const resolvedParams = use(params);
const [dashboard, setDashboard] = useState<{ const [dashboard, setDashboard] = useState<{
id: string; id: string;
title: string; title: string;
@ -31,7 +32,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
// 대시보드 데이터 로딩 // 대시보드 데이터 로딩
useEffect(() => { useEffect(() => {
loadDashboard(); loadDashboard();
}, [params.dashboardId]); }, [resolvedParams.dashboardId]);
const loadDashboard = async () => { const loadDashboard = async () => {
setIsLoading(true); setIsLoading(true);
@ -39,29 +40,29 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
try { try {
// 실제 API 호출 시도 // 실제 API 호출 시도
const { dashboardApi } = await import('@/lib/api/dashboard'); const { dashboardApi } = await import("@/lib/api/dashboard");
try { try {
const dashboardData = await dashboardApi.getDashboard(params.dashboardId); const dashboardData = await dashboardApi.getDashboard(resolvedParams.dashboardId);
setDashboard(dashboardData); setDashboard(dashboardData);
} catch (apiError) { } catch (apiError) {
console.warn('API 호출 실패, 로컬 스토리지 확인:', apiError); console.warn("API 호출 실패, 로컬 스토리지 확인:", apiError);
// API 실패 시 로컬 스토리지에서 찾기 // API 실패 시 로컬 스토리지에서 찾기
const savedDashboards = JSON.parse(localStorage.getItem('savedDashboards') || '[]'); const savedDashboards = JSON.parse(localStorage.getItem("savedDashboards") || "[]");
const savedDashboard = savedDashboards.find((d: any) => d.id === params.dashboardId); const savedDashboard = savedDashboards.find((d: any) => d.id === resolvedParams.dashboardId);
if (savedDashboard) { if (savedDashboard) {
setDashboard(savedDashboard); setDashboard(savedDashboard);
} else { } else {
// 로컬에도 없으면 샘플 데이터 사용 // 로컬에도 없으면 샘플 데이터 사용
const sampleDashboard = generateSampleDashboard(params.dashboardId); const sampleDashboard = generateSampleDashboard(resolvedParams.dashboardId);
setDashboard(sampleDashboard); setDashboard(sampleDashboard);
} }
} }
} catch (err) { } catch (err) {
setError('대시보드를 불러오는 중 오류가 발생했습니다.'); setError("대시보드를 불러오는 중 오류가 발생했습니다.");
console.error('Dashboard loading error:', err); console.error("Dashboard loading error:", err);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -70,11 +71,11 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
// 로딩 상태 // 로딩 상태
if (isLoading) { if (isLoading) {
return ( return (
<div className="h-screen flex items-center justify-center bg-gray-50"> <div className="flex h-screen items-center justify-center bg-gray-50">
<div className="text-center"> <div className="text-center">
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
<div className="text-lg font-medium text-gray-700"> ...</div> <div className="text-lg font-medium text-gray-700"> ...</div>
<div className="text-sm text-gray-500 mt-1"> </div> <div className="mt-1 text-sm text-gray-500"> </div>
</div> </div>
</div> </div>
); );
@ -83,19 +84,12 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
// 에러 상태 // 에러 상태
if (error || !dashboard) { if (error || !dashboard) {
return ( return (
<div className="h-screen flex items-center justify-center bg-gray-50"> <div className="flex h-screen items-center justify-center bg-gray-50">
<div className="text-center"> <div className="text-center">
<div className="text-6xl mb-4">😞</div> <div className="mb-4 text-6xl">😞</div>
<div className="text-xl font-medium text-gray-700 mb-2"> <div className="mb-2 text-xl font-medium text-gray-700">{error || "대시보드를 찾을 수 없습니다"}</div>
{error || '대시보드를 찾을 수 없습니다'} <div className="mb-4 text-sm text-gray-500"> ID: {resolvedParams.dashboardId}</div>
</div> <button onClick={loadDashboard} className="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
<div className="text-sm text-gray-500 mb-4">
ID: {params.dashboardId}
</div>
<button
onClick={loadDashboard}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
</button> </button>
</div> </div>
@ -106,25 +100,23 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
return ( return (
<div className="h-screen bg-gray-50"> <div className="h-screen bg-gray-50">
{/* 대시보드 헤더 */} {/* 대시보드 헤더 */}
<div className="bg-white border-b border-gray-200 px-6 py-4"> <div className="border-b border-gray-200 bg-white px-6 py-4">
<div className="flex justify-between items-center"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-gray-800">{dashboard.title}</h1> <h1 className="text-2xl font-bold text-gray-800">{dashboard.title}</h1>
{dashboard.description && ( {dashboard.description && <p className="mt-1 text-sm text-gray-600">{dashboard.description}</p>}
<p className="text-sm text-gray-600 mt-1">{dashboard.description}</p>
)}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* 새로고침 버튼 */} {/* 새로고침 버튼 */}
<button <button
onClick={loadDashboard} onClick={loadDashboard}
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="새로고침" title="새로고침"
> >
🔄 🔄
</button> </button>
{/* 전체화면 버튼 */} {/* 전체화면 버튼 */}
<button <button
onClick={() => { onClick={() => {
@ -134,26 +126,26 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
document.documentElement.requestFullscreen(); 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="전체화면" title="전체화면"
> >
</button> </button>
{/* 편집 버튼 */} {/* 편집 버튼 */}
<button <button
onClick={() => { onClick={() => {
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"
> >
</button> </button>
</div> </div>
</div> </div>
{/* 메타 정보 */} {/* 메타 정보 */}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500"> <div className="mt-2 flex items-center gap-4 text-xs text-gray-500">
<span>: {new Date(dashboard.createdAt).toLocaleString()}</span> <span>: {new Date(dashboard.createdAt).toLocaleString()}</span>
<span>: {new Date(dashboard.updatedAt).toLocaleString()}</span> <span>: {new Date(dashboard.updatedAt).toLocaleString()}</span>
<span>: {dashboard.elements.length}</span> <span>: {dashboard.elements.length}</span>
@ -162,10 +154,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
{/* 대시보드 뷰어 */} {/* 대시보드 뷰어 */}
<div className="h-[calc(100vh-120px)]"> <div className="h-[calc(100vh-120px)]">
<DashboardViewer <DashboardViewer elements={dashboard.elements} dashboardId={dashboard.id} />
elements={dashboard.elements}
dashboardId={dashboard.id}
/>
</div> </div>
</div> </div>
); );
@ -176,111 +165,113 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
*/ */
function generateSampleDashboard(dashboardId: string) { function generateSampleDashboard(dashboardId: string) {
const dashboards: Record<string, any> = { const dashboards: Record<string, any> = {
'sales-overview': { "sales-overview": {
id: 'sales-overview', id: "sales-overview",
title: '📊 매출 현황 대시보드', title: "📊 매출 현황 대시보드",
description: '월별 매출 추이 및 상품별 판매 현황을 한눈에 확인할 수 있습니다.', description: "월별 매출 추이 및 상품별 판매 현황을 한눈에 확인할 수 있습니다.",
elements: [ elements: [
{ {
id: 'chart-1', id: "chart-1",
type: 'chart', type: "chart",
subtype: 'bar', subtype: "bar",
position: { x: 20, y: 20 }, position: { x: 20, y: 20 },
size: { width: 400, height: 300 }, size: { width: 400, height: 300 },
title: '📊 월별 매출 추이', title: "📊 월별 매출 추이",
content: '월별 매출 데이터', content: "월별 매출 데이터",
dataSource: { dataSource: {
type: 'database', type: "database",
query: 'SELECT month, sales FROM monthly_sales', query: "SELECT month, sales FROM monthly_sales",
refreshInterval: 30000 refreshInterval: 30000,
}, },
chartConfig: { chartConfig: {
xAxis: 'month', xAxis: "month",
yAxis: 'sales', yAxis: "sales",
title: '월별 매출 추이', title: "월별 매출 추이",
colors: ['#3B82F6', '#EF4444', '#10B981'] colors: ["#3B82F6", "#EF4444", "#10B981"],
} },
}, },
{ {
id: 'chart-2', id: "chart-2",
type: 'chart', type: "chart",
subtype: 'pie', subtype: "pie",
position: { x: 450, y: 20 }, position: { x: 450, y: 20 },
size: { width: 350, height: 300 }, size: { width: 350, height: 300 },
title: '🥧 상품별 판매 비율', title: "🥧 상품별 판매 비율",
content: '상품별 판매 데이터', content: "상품별 판매 데이터",
dataSource: { dataSource: {
type: 'database', type: "database",
query: 'SELECT product_name, total_sold FROM product_sales', query: "SELECT product_name, total_sold FROM product_sales",
refreshInterval: 60000 refreshInterval: 60000,
}, },
chartConfig: { chartConfig: {
xAxis: 'product_name', xAxis: "product_name",
yAxis: 'total_sold', yAxis: "total_sold",
title: '상품별 판매 비율', title: "상품별 판매 비율",
colors: ['#8B5CF6', '#EC4899', '#06B6D4', '#84CC16'] colors: ["#8B5CF6", "#EC4899", "#06B6D4", "#84CC16"],
} },
}, },
{ {
id: 'chart-3', id: "chart-3",
type: 'chart', type: "chart",
subtype: 'line', subtype: "line",
position: { x: 20, y: 350 }, position: { x: 20, y: 350 },
size: { width: 780, height: 250 }, size: { width: 780, height: 250 },
title: '📈 사용자 가입 추이', title: "📈 사용자 가입 추이",
content: '사용자 가입 데이터', content: "사용자 가입 데이터",
dataSource: { dataSource: {
type: 'database', type: "database",
query: 'SELECT week, new_users FROM user_growth', query: "SELECT week, new_users FROM user_growth",
refreshInterval: 300000 refreshInterval: 300000,
}, },
chartConfig: { chartConfig: {
xAxis: 'week', xAxis: "week",
yAxis: 'new_users', yAxis: "new_users",
title: '주간 신규 사용자 가입 추이', title: "주간 신규 사용자 가입 추이",
colors: ['#10B981'] colors: ["#10B981"],
} },
} },
], ],
createdAt: '2024-09-30T10:00:00Z', createdAt: "2024-09-30T10:00:00Z",
updatedAt: '2024-09-30T14:30:00Z' updatedAt: "2024-09-30T14:30:00Z",
}, },
'user-analytics': { "user-analytics": {
id: 'user-analytics', id: "user-analytics",
title: '👥 사용자 분석 대시보드', title: "👥 사용자 분석 대시보드",
description: '사용자 행동 패턴 및 가입 추이 분석', description: "사용자 행동 패턴 및 가입 추이 분석",
elements: [ elements: [
{ {
id: 'chart-4', id: "chart-4",
type: 'chart', type: "chart",
subtype: 'line', subtype: "line",
position: { x: 20, y: 20 }, position: { x: 20, y: 20 },
size: { width: 500, height: 300 }, size: { width: 500, height: 300 },
title: '📈 일일 활성 사용자', title: "📈 일일 활성 사용자",
content: '사용자 활동 데이터', content: "사용자 활동 데이터",
dataSource: { dataSource: {
type: 'database', type: "database",
query: 'SELECT date, active_users FROM daily_active_users', query: "SELECT date, active_users FROM daily_active_users",
refreshInterval: 60000 refreshInterval: 60000,
}, },
chartConfig: { chartConfig: {
xAxis: 'date', xAxis: "date",
yAxis: 'active_users', yAxis: "active_users",
title: '일일 활성 사용자 추이' title: "일일 활성 사용자 추이",
} },
} },
], ],
createdAt: '2024-09-29T15:00:00Z', createdAt: "2024-09-29T15:00:00Z",
updatedAt: '2024-09-30T09:15:00Z' updatedAt: "2024-09-30T09:15:00Z",
} },
}; };
return dashboards[dashboardId] || { return (
id: dashboardId, dashboards[dashboardId] || {
title: `대시보드 ${dashboardId}`, id: dashboardId,
description: '샘플 대시보드입니다.', title: `대시보드 ${dashboardId}`,
elements: [], description: "샘플 대시보드입니다.",
createdAt: new Date().toISOString(), elements: [],
updatedAt: new Date().toISOString() createdAt: new Date().toISOString(),
}; updatedAt: new Date().toISOString(),
}
);
} }

View File

@ -1,8 +1,8 @@
'use client'; "use client";
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from "react";
import { DashboardElement, QueryResult } from '@/components/admin/dashboard/types'; import { DashboardElement, QueryResult } from "@/components/admin/dashboard/types";
import { ChartRenderer } from '@/components/admin/dashboard/charts/ChartRenderer'; import { ChartRenderer } from "@/components/admin/dashboard/charts/ChartRenderer";
interface DashboardViewerProps { interface DashboardViewerProps {
elements: DashboardElement[]; elements: DashboardElement[];
@ -23,36 +23,60 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
// 개별 요소 데이터 로딩 // 개별 요소 데이터 로딩
const loadElementData = useCallback(async (element: DashboardElement) => { const loadElementData = useCallback(async (element: DashboardElement) => {
if (!element.dataSource?.query || element.type !== 'chart') { if (!element.dataSource?.query || element.type !== "chart") {
return; return;
} }
setLoadingElements(prev => new Set([...prev, element.id])); setLoadingElements((prev) => new Set([...prev, element.id]));
try { try {
// console.log(`🔄 요소 ${element.id} 데이터 로딩 시작:`, element.dataSource.query); let result;
// 실제 API 호출 // 외부 DB vs 현재 DB 분기
const { dashboardApi } = await import('@/lib/api/dashboard'); if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) {
const result = await dashboardApi.executeQuery(element.dataSource.query); // 외부 DB
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
// console.log(`✅ 요소 ${element.id} 데이터 로딩 완료:`, result); const externalResult = await ExternalDbConnectionAPI.executeQuery(
parseInt(element.dataSource.externalConnectionId),
const data: QueryResult = { element.dataSource.query,
columns: result.columns || [], );
rows: result.rows || [],
totalRows: result.rowCount || 0, if (!externalResult.success) {
executionTime: 0 throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패");
}; }
setElementData(prev => ({ const data: QueryResult = {
...prev, columns: externalResult.data?.[0] ? Object.keys(externalResult.data[0]) : [],
[element.id]: data 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) { } catch (error) {
// console.error(`❌ Element ${element.id} data loading error:`, error); // 에러 발생 시 무시 (차트는 빈 상태로 표시됨)
} finally { } finally {
setLoadingElements(prev => { setLoadingElements((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.delete(element.id); newSet.delete(element.id);
return newSet; return newSet;
@ -63,11 +87,11 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
// 모든 요소 데이터 로딩 // 모든 요소 데이터 로딩
const loadAllData = useCallback(async () => { const loadAllData = useCallback(async () => {
setLastRefresh(new Date()); 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]); }, [elements, loadElementData]);
// 초기 데이터 로딩 // 초기 데이터 로딩
@ -88,34 +112,28 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
// 요소가 없는 경우 // 요소가 없는 경우
if (elements.length === 0) { if (elements.length === 0) {
return ( 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-center">
<div className="text-6xl mb-4">📊</div> <div className="mb-4 text-6xl">📊</div>
<div className="text-xl font-medium text-gray-700 mb-2"> <div className="mb-2 text-xl font-medium text-gray-700"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
<div className="text-sm text-gray-500">
</div>
</div> </div>
</div> </div>
); );
} }
return ( 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()} : {lastRefresh.toLocaleTimeString()}
{Array.from(loadingElements).length > 0 && ( {Array.from(loadingElements).length > 0 && (
<span className="ml-2 text-primary"> <span className="text-primary ml-2">({Array.from(loadingElements).length} ...)</span>
({Array.from(loadingElements).length} ...)
</span>
)} )}
</div> </div>
{/* 대시보드 요소들 */} {/* 대시보드 요소들 */}
<div className="relative" style={{ minHeight: '100%' }}> <div className="relative" style={{ minHeight: "100%" }}>
{elements.map((element) => ( {elements.map((element) => (
<ViewerElement <ViewerElement
key={element.id} key={element.id}
@ -145,32 +163,32 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
return ( return (
<div <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={{ style={{
left: element.position.x, left: element.position.x,
top: element.position.y, top: element.position.y,
width: element.size.width, width: element.size.width,
height: element.size.height height: element.size.height,
}} }}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
{/* 헤더 */} {/* 헤더 */}
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center"> <div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
<h3 className="font-semibold text-gray-800 text-sm">{element.title}</h3> <h3 className="text-sm font-semibold text-gray-800">{element.title}</h3>
{/* 새로고침 버튼 (호버 시에만 표시) */} {/* 새로고침 버튼 (호버 시에만 표시) */}
{isHovered && ( {isHovered && (
<button <button
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} 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="새로고침" title="새로고침"
> >
{isLoading ? ( {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> </button>
)} )}
@ -178,20 +196,15 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
{/* 내용 */} {/* 내용 */}
<div className="h-[calc(100%-57px)]"> <div className="h-[calc(100%-57px)]">
{element.type === 'chart' ? ( {element.type === "chart" ? (
<ChartRenderer <ChartRenderer element={element} data={data} width={element.size.width} height={element.size.height - 57} />
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-center">
<div className="text-3xl mb-2"> <div className="mb-2 text-3xl">
{element.subtype === 'exchange' && '💱'} {element.subtype === "exchange" && "💱"}
{element.subtype === 'weather' && '☁️'} {element.subtype === "weather" && "☁️"}
</div> </div>
<div className="text-sm whitespace-pre-line">{element.content}</div> <div className="text-sm whitespace-pre-line">{element.content}</div>
</div> </div>
@ -201,10 +214,10 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
{/* 로딩 오버레이 */} {/* 로딩 오버레이 */}
{isLoading && ( {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="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="border-primary mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
<div className="text-sm text-muted-foreground"> ...</div> <div className="text-muted-foreground text-sm"> ...</div>
</div> </div>
</div> </div>
)} )}
@ -218,53 +231,73 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
function generateSampleQueryResult(query: string, chartType: string): QueryResult { function generateSampleQueryResult(query: string, chartType: string): QueryResult {
// 시간에 따라 약간씩 다른 데이터 생성 (실시간 업데이트 시뮬레이션) // 시간에 따라 약간씩 다른 데이터 생성 (실시간 업데이트 시뮬레이션)
const timeVariation = Math.sin(Date.now() / 10000) * 0.1 + 1; const timeVariation = Math.sin(Date.now() / 10000) * 0.1 + 1;
const isMonthly = query.toLowerCase().includes('month'); const isMonthly = query.toLowerCase().includes("month");
const isSales = query.toLowerCase().includes('sales') || query.toLowerCase().includes('매출'); const isSales = query.toLowerCase().includes("sales") || query.toLowerCase().includes("매출");
const isUsers = query.toLowerCase().includes('users') || query.toLowerCase().includes('사용자'); const isUsers = query.toLowerCase().includes("users") || query.toLowerCase().includes("사용자");
const isProducts = query.toLowerCase().includes('product') || query.toLowerCase().includes('상품'); const isProducts = query.toLowerCase().includes("product") || query.toLowerCase().includes("상품");
const isWeekly = query.toLowerCase().includes('week'); const isWeekly = query.toLowerCase().includes("week");
let columns: string[]; let columns: string[];
let rows: Record<string, any>[]; let rows: Record<string, any>[];
if (isMonthly && isSales) { if (isMonthly && isSales) {
columns = ['month', 'sales', 'order_count']; columns = ["month", "sales", "order_count"];
rows = [ rows = [
{ month: '2024-01', sales: Math.round(1200000 * timeVariation), order_count: Math.round(45 * 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-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-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-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-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-06", sales: Math.round(1540000 * timeVariation), order_count: Math.round(61 * timeVariation) },
]; ];
} else if (isWeekly && isUsers) { } else if (isWeekly && isUsers) {
columns = ['week', 'new_users']; columns = ["week", "new_users"];
rows = [ rows = [
{ week: '2024-W10', new_users: Math.round(23 * timeVariation) }, { week: "2024-W10", new_users: Math.round(23 * timeVariation) },
{ week: '2024-W11', new_users: Math.round(31 * timeVariation) }, { week: "2024-W11", new_users: Math.round(31 * timeVariation) },
{ week: '2024-W12', new_users: Math.round(28 * timeVariation) }, { week: "2024-W12", new_users: Math.round(28 * timeVariation) },
{ week: '2024-W13', new_users: Math.round(35 * timeVariation) }, { week: "2024-W13", new_users: Math.round(35 * timeVariation) },
{ week: '2024-W14', new_users: Math.round(42 * timeVariation) }, { week: "2024-W14", new_users: Math.round(42 * timeVariation) },
{ week: '2024-W15', new_users: Math.round(38 * timeVariation) }, { week: "2024-W15", new_users: Math.round(38 * timeVariation) },
]; ];
} else if (isProducts) { } else if (isProducts) {
columns = ['product_name', 'total_sold', 'revenue']; columns = ["product_name", "total_sold", "revenue"];
rows = [ 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: "스마트폰",
{ product_name: '태블릿', total_sold: Math.round(134 * timeVariation), revenue: Math.round(67000000 * timeVariation) }, total_sold: Math.round(156 * timeVariation),
{ product_name: '이어폰', total_sold: Math.round(267 * timeVariation), revenue: Math.round(26700000 * timeVariation) }, revenue: Math.round(234000000 * timeVariation),
{ product_name: '스마트워치', total_sold: Math.round(98 * timeVariation), revenue: Math.round(49000000 * 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 { } else {
columns = ['category', 'value', 'count']; columns = ["category", "value", "count"];
rows = [ rows = [
{ category: 'A', value: Math.round(100 * timeVariation), count: Math.round(10 * 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: "B", value: Math.round(150 * timeVariation), count: Math.round(15 * timeVariation) },
{ category: 'C', value: Math.round(120 * timeVariation), count: Math.round(12 * 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: "D", value: Math.round(180 * timeVariation), count: Math.round(18 * timeVariation) },
{ category: 'E', value: Math.round(90 * timeVariation), count: Math.round(9 * timeVariation) }, { category: "E", value: Math.round(90 * timeVariation), count: Math.round(9 * timeVariation) },
]; ];
} }