오류 해결 및 마지막 업데이트 삭제
This commit is contained in:
parent
cb224be93e
commit
2ff0d77e62
|
|
@ -27,6 +27,10 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
||||||
elements: DashboardElement[];
|
elements: DashboardElement[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
settings?: {
|
||||||
|
resolution?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
};
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -102,7 +106,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-gray-50">
|
<div className="h-screen bg-gray-50">
|
||||||
{/* 대시보드 뷰어 - 전체 화면 */}
|
{/* 대시보드 뷰어 - 전체 화면 */}
|
||||||
<DashboardViewer elements={dashboard.elements} dashboardId={dashboard.id} />
|
<DashboardViewer elements={dashboard.elements} resolution={dashboard.settings?.resolution || "fhd"} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } 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";
|
||||||
|
import { RESOLUTIONS, Resolution } from "@/components/admin/dashboard/ResolutionSelector";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// 위젯 동적 import - 모든 위젯
|
// 위젯 동적 import - 모든 위젯
|
||||||
const ListSummaryWidget = dynamic(() => import("./widgets/ListSummaryWidget"), { ssr: false });
|
|
||||||
const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false });
|
const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false });
|
||||||
const StatusSummaryWidget = dynamic(() => import("./widgets/StatusSummaryWidget"), { ssr: false });
|
const StatusSummaryWidget = dynamic(() => import("./widgets/StatusSummaryWidget"), { ssr: false });
|
||||||
const RiskAlertWidget = dynamic(() => import("./widgets/RiskAlertWidget"), { ssr: false });
|
const RiskAlertWidget = dynamic(() => import("./widgets/RiskAlertWidget"), { ssr: false });
|
||||||
|
|
@ -65,8 +65,6 @@ function renderWidget(element: DashboardElement) {
|
||||||
return <ClockWidget element={element} />;
|
return <ClockWidget element={element} />;
|
||||||
case "map-summary":
|
case "map-summary":
|
||||||
return <MapSummaryWidget element={element} />;
|
return <MapSummaryWidget element={element} />;
|
||||||
case "list-summary":
|
|
||||||
return <ListSummaryWidget element={element} />;
|
|
||||||
case "risk-alert":
|
case "risk-alert":
|
||||||
return <RiskAlertWidget element={element} />;
|
return <RiskAlertWidget element={element} />;
|
||||||
case "calendar":
|
case "calendar":
|
||||||
|
|
@ -80,7 +78,7 @@ function renderWidget(element: DashboardElement) {
|
||||||
case "booking-alert":
|
case "booking-alert":
|
||||||
return <BookingAlertWidget element={element} />;
|
return <BookingAlertWidget element={element} />;
|
||||||
case "maintenance":
|
case "maintenance":
|
||||||
return <MaintenanceWidget element={element} />;
|
return <MaintenanceWidget />;
|
||||||
case "document":
|
case "document":
|
||||||
return <DocumentWidget element={element} />;
|
return <DocumentWidget element={element} />;
|
||||||
case "list":
|
case "list":
|
||||||
|
|
@ -91,23 +89,23 @@ function renderWidget(element: DashboardElement) {
|
||||||
|
|
||||||
// === 차량 관련 (추가 위젯) ===
|
// === 차량 관련 (추가 위젯) ===
|
||||||
case "vehicle-status":
|
case "vehicle-status":
|
||||||
return <VehicleStatusWidget />;
|
return <VehicleStatusWidget element={element} />;
|
||||||
case "vehicle-list":
|
case "vehicle-list":
|
||||||
return <VehicleListWidget />;
|
return <VehicleListWidget element={element} />;
|
||||||
case "vehicle-map":
|
case "vehicle-map":
|
||||||
return <VehicleMapOnlyWidget element={element} />;
|
return <VehicleMapOnlyWidget element={element} />;
|
||||||
|
|
||||||
// === 배송 관련 (추가 위젯) ===
|
// === 배송 관련 (추가 위젯) ===
|
||||||
case "delivery-status":
|
case "delivery-status":
|
||||||
return <DeliveryStatusWidget />;
|
return <DeliveryStatusWidget element={element} />;
|
||||||
case "delivery-status-summary":
|
case "delivery-status-summary":
|
||||||
return <DeliveryStatusSummaryWidget />;
|
return <DeliveryStatusSummaryWidget element={element} />;
|
||||||
case "delivery-today-stats":
|
case "delivery-today-stats":
|
||||||
return <DeliveryTodayStatsWidget />;
|
return <DeliveryTodayStatsWidget element={element} />;
|
||||||
case "cargo-list":
|
case "cargo-list":
|
||||||
return <CargoListWidget />;
|
return <CargoListWidget element={element} />;
|
||||||
case "customer-issues":
|
case "customer-issues":
|
||||||
return <CustomerIssuesWidget />;
|
return <CustomerIssuesWidget element={element} />;
|
||||||
|
|
||||||
// === 기본 fallback ===
|
// === 기본 fallback ===
|
||||||
default:
|
default:
|
||||||
|
|
@ -124,20 +122,33 @@ function renderWidget(element: DashboardElement) {
|
||||||
|
|
||||||
interface DashboardViewerProps {
|
interface DashboardViewerProps {
|
||||||
elements: DashboardElement[];
|
elements: DashboardElement[];
|
||||||
dashboardId: string;
|
|
||||||
refreshInterval?: number; // 전체 대시보드 새로고침 간격 (ms)
|
refreshInterval?: number; // 전체 대시보드 새로고침 간격 (ms)
|
||||||
|
resolution?: string; // 대시보드 해상도
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 대시보드 뷰어 컴포넌트
|
* 대시보드 뷰어 컴포넌트
|
||||||
* - 저장된 대시보드를 읽기 전용으로 표시
|
* - 저장된 대시보드를 읽기 전용으로 표시
|
||||||
* - 실시간 데이터 업데이트
|
* - 실시간 데이터 업데이트
|
||||||
* - 반응형 레이아웃
|
* - 편집 화면과 동일한 레이아웃 (중앙 정렬, 고정 크기)
|
||||||
*/
|
*/
|
||||||
export function DashboardViewer({ elements, dashboardId, refreshInterval }: DashboardViewerProps) {
|
export function DashboardViewer({ elements, refreshInterval, resolution = "fhd" }: DashboardViewerProps) {
|
||||||
const [elementData, setElementData] = useState<Record<string, QueryResult>>({});
|
const [elementData, setElementData] = useState<Record<string, QueryResult>>({});
|
||||||
const [loadingElements, setLoadingElements] = useState<Set<string>>(new Set());
|
const [loadingElements, setLoadingElements] = useState<Set<string>>(new Set());
|
||||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
|
||||||
|
// 캔버스 설정 계산
|
||||||
|
const canvasConfig = useMemo(() => {
|
||||||
|
return RESOLUTIONS[resolution as Resolution] || RESOLUTIONS.fhd;
|
||||||
|
}, [resolution]);
|
||||||
|
|
||||||
|
// 캔버스 높이 동적 계산
|
||||||
|
const canvasHeight = useMemo(() => {
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return canvasConfig.height;
|
||||||
|
}
|
||||||
|
const maxBottomY = Math.max(...elements.map((el) => el.position.y + el.size.height));
|
||||||
|
return Math.max(canvasConfig.height, maxBottomY + 100);
|
||||||
|
}, [elements, canvasConfig.height]);
|
||||||
|
|
||||||
// 개별 요소 데이터 로딩
|
// 개별 요소 데이터 로딩
|
||||||
const loadElementData = useCallback(async (element: DashboardElement) => {
|
const loadElementData = useCallback(async (element: DashboardElement) => {
|
||||||
|
|
@ -191,7 +202,7 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
|
||||||
[element.id]: data,
|
[element.id]: data,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// 에러 발생 시 무시 (차트는 빈 상태로 표시됨)
|
// 에러 발생 시 무시 (차트는 빈 상태로 표시됨)
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingElements((prev) => {
|
setLoadingElements((prev) => {
|
||||||
|
|
@ -204,8 +215,6 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
|
||||||
|
|
||||||
// 모든 요소 데이터 로딩
|
// 모든 요소 데이터 로딩
|
||||||
const loadAllData = useCallback(async () => {
|
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);
|
||||||
|
|
||||||
// 병렬로 모든 차트 데이터 로딩
|
// 병렬로 모든 차트 데이터 로딩
|
||||||
|
|
@ -241,17 +250,17 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full overflow-auto bg-gray-100">
|
<div className="flex h-full items-start justify-center overflow-auto bg-gray-100 p-8">
|
||||||
{/* 새로고침 상태 표시 */}
|
{/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */}
|
||||||
<div className="text-muted-foreground absolute top-4 right-4 z-10 rounded-lg bg-white px-3 py-2 text-xs shadow-sm">
|
<div
|
||||||
마지막 업데이트: {lastRefresh.toLocaleTimeString()}
|
className="relative overflow-hidden rounded-lg"
|
||||||
{Array.from(loadingElements).length > 0 && (
|
style={{
|
||||||
<span className="text-primary ml-2">({Array.from(loadingElements).length}개 로딩 중...)</span>
|
width: `${canvasConfig.width}px`,
|
||||||
)}
|
minHeight: `${canvasConfig.height}px`,
|
||||||
</div>
|
height: `${canvasHeight}px`,
|
||||||
|
}}
|
||||||
{/* 대시보드 요소들 */}
|
>
|
||||||
<div className="relative" style={{ minHeight: "100%" }}>
|
{/* 대시보드 요소들 */}
|
||||||
{elements.map((element) => (
|
{elements.map((element) => (
|
||||||
<ViewerElement
|
<ViewerElement
|
||||||
key={element.id}
|
key={element.id}
|
||||||
|
|
@ -333,87 +342,3 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 샘플 쿼리 결과 생성 함수 (뷰어용)
|
|
||||||
*/
|
|
||||||
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");
|
|
||||||
|
|
||||||
let columns: string[];
|
|
||||||
let rows: Record<string, any>[];
|
|
||||||
|
|
||||||
if (isMonthly && isSales) {
|
|
||||||
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) },
|
|
||||||
];
|
|
||||||
} else if (isWeekly && isUsers) {
|
|
||||||
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) },
|
|
||||||
];
|
|
||||||
} else if (isProducts) {
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
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) },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
totalRows: rows.length,
|
|
||||||
executionTime: Math.floor(Math.random() * 100) + 50,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue