에러 해결

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 { 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 (
<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="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-sm text-gray-500 mt-1"> </div>
<div className="mt-1 text-sm text-gray-500"> </div>
</div>
</div>
);
@ -83,19 +84,12 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
// 에러 상태
if (error || !dashboard) {
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-6xl mb-4">😞</div>
<div className="text-xl font-medium text-gray-700 mb-2">
{error || '대시보드를 찾을 수 없습니다'}
</div>
<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"
>
<div className="mb-4 text-6xl">😞</div>
<div className="mb-2 text-xl font-medium text-gray-700">{error || "대시보드를 찾을 수 없습니다"}</div>
<div className="mb-4 text-sm text-gray-500"> ID: {resolvedParams.dashboardId}</div>
<button onClick={loadDashboard} className="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
</button>
</div>
@ -106,25 +100,23 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
return (
<div className="h-screen bg-gray-50">
{/* 대시보드 헤더 */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<div className="flex justify-between items-center">
<div className="border-b border-gray-200 bg-white px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-800">{dashboard.title}</h1>
{dashboard.description && (
<p className="text-sm text-gray-600 mt-1">{dashboard.description}</p>
)}
{dashboard.description && <p className="mt-1 text-sm text-gray-600">{dashboard.description}</p>}
</div>
<div className="flex items-center gap-3">
{/* 새로고침 버튼 */}
<button
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="새로고침"
>
🔄
</button>
{/* 전체화면 버튼 */}
<button
onClick={() => {
@ -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="전체화면"
>
</button>
{/* 편집 버튼 */}
<button
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>
</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.updatedAt).toLocaleString()}</span>
<span>: {dashboard.elements.length}</span>
@ -162,10 +154,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
{/* 대시보드 뷰어 */}
<div className="h-[calc(100vh-120px)]">
<DashboardViewer
elements={dashboard.elements}
dashboardId={dashboard.id}
/>
<DashboardViewer elements={dashboard.elements} dashboardId={dashboard.id} />
</div>
</div>
);
@ -176,111 +165,113 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
*/
function generateSampleDashboard(dashboardId: string) {
const dashboards: Record<string, any> = {
'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(),
}
);
}

View File

@ -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) },
];
}