diff --git a/frontend/app/(main)/admin/dashboard/DashboardListClient.tsx b/frontend/app/(main)/admin/dashboard/DashboardListClient.tsx index a67761e4..d8eeae61 100644 --- a/frontend/app/(main)/admin/dashboard/DashboardListClient.tsx +++ b/frontend/app/(main)/admin/dashboard/DashboardListClient.tsx @@ -181,14 +181,19 @@ export default function DashboardListClient({ initialDashboards, initialPaginati <> {/* 검색 및 액션 */}
-
- - setSearchTerm(e.target.value)} - className="h-10 pl-10 text-sm" - /> +
+
+ + setSearchTerm(e.target.value)} + className="h-10 pl-10 text-sm" + /> +
+
+ 총 {totalCount.toLocaleString()} 건 +
+ + + router.push(`/admin/dashboard/edit/${dashboard.id}`)} + className="gap-2 text-sm" + > + + 편집 + + handleCopy(dashboard)} className="gap-2 text-sm"> + + 복사 + + handleDeleteClick(dashboard.id, dashboard.title)} + className="text-destructive focus:text-destructive gap-2 text-sm" + > + + 삭제 + + + + + + ))} + + +
+ + {/* 모바일/태블릿 카드 뷰 (lg 미만) */} +
+ {dashboards.map((dashboard) => ( +
+ {/* 헤더 */} +
+
+

{dashboard.title}

+

{dashboard.id}

+
+
+ + {/* 정보 */} +
+
+ 설명 + {dashboard.description || "-"} +
+
+ 생성일 + {formatDate(dashboard.createdAt)} +
+
+ 수정일 + {formatDate(dashboard.updatedAt)} +
+
+ + {/* 액션 */} +
+ + + +
+
+ ))} +
+ )} {/* 페이지네이션 */} diff --git a/frontend/app/(main)/admin/dashboard/new/page.tsx b/frontend/app/(main)/admin/dashboard/new/page.tsx index d2f2ce11..56d28f46 100644 --- a/frontend/app/(main)/admin/dashboard/new/page.tsx +++ b/frontend/app/(main)/admin/dashboard/new/page.tsx @@ -1,6 +1,3 @@ -"use client"; - -import React from "react"; import DashboardDesigner from "@/components/admin/dashboard/DashboardDesigner"; /** diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index e643b9bd..39546d91 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -7,14 +7,7 @@ import { DashboardTopMenu } from "./DashboardTopMenu"; import { ElementConfigSidebar } from "./ElementConfigSidebar"; import { DashboardSaveModal } from "./DashboardSaveModal"; import { DashboardElement, ElementType, ElementSubtype } from "./types"; -import { - GRID_CONFIG, - snapToGrid, - snapSizeToGrid, - calculateCellSize, - calculateGridConfig, - calculateBoxSize, -} from "./gridUtils"; +import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize, calculateBoxSize } from "./gridUtils"; import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector"; import { DashboardProvider } from "@/contexts/DashboardContext"; import { useMenu } from "@/contexts/MenuContext"; @@ -199,7 +192,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D ...el, dataSources: el.chartConfig?.dataSources || el.dataSources, })); - + setElements(elementsWithDataSources); // elementCounter를 가장 큰 ID 번호로 설정 @@ -582,11 +575,11 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D // 로딩 중이면 로딩 화면 표시 if (isLoading) { return ( -
+
-
대시보드 로딩 중...
-
잠시만 기다려주세요
+
대시보드 로딩 중...
+
잠시만 기다려주세요
); @@ -594,7 +587,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D return ( -
+
{/* 상단 메뉴바 */} +
-
- +
+
저장 완료 대시보드가 성공적으로 저장되었습니다. diff --git a/frontend/components/admin/dashboard/DashboardTopMenu.tsx b/frontend/components/admin/dashboard/DashboardTopMenu.tsx index 611c7a97..7c9505e2 100644 --- a/frontend/components/admin/dashboard/DashboardTopMenu.tsx +++ b/frontend/components/admin/dashboard/DashboardTopMenu.tsx @@ -78,10 +78,9 @@ export function DashboardTopMenu({ dataUrl: string, format: "png" | "pdf", canvasWidth: number, - canvasHeight: number + canvasHeight: number, ) => { if (format === "png") { - console.log("💾 PNG 다운로드 시작..."); const link = document.createElement("a"); const filename = `${dashboardTitle || "dashboard"}_${new Date().toISOString().split("T")[0]}.png`; link.download = filename; @@ -89,11 +88,9 @@ export function DashboardTopMenu({ document.body.appendChild(link); link.click(); document.body.removeChild(link); - console.log("✅ PNG 다운로드 완료:", filename); } else { - console.log("📄 PDF 생성 중..."); const jsPDF = (await import("jspdf")).default; - + // dataUrl에서 이미지 크기 계산 const img = new Image(); img.src = dataUrl; @@ -101,17 +98,12 @@ export function DashboardTopMenu({ img.onload = resolve; }); - console.log("📐 이미지 실제 크기:", { width: img.width, height: img.height }); - console.log("📐 캔버스 계산 크기:", { width: canvasWidth, height: canvasHeight }); - // PDF 크기 계산 (A4 기준) const imgWidth = 210; // A4 width in mm const actualHeight = canvasHeight; const actualWidth = canvasWidth; const imgHeight = (actualHeight * imgWidth) / actualWidth; - console.log("📄 PDF 크기:", { width: imgWidth, height: imgHeight }); - const pdf = new jsPDF({ orientation: imgHeight > imgWidth ? "portrait" : "landscape", unit: "mm", @@ -121,53 +113,44 @@ export function DashboardTopMenu({ pdf.addImage(dataUrl, "PNG", 0, 0, imgWidth, imgHeight); const filename = `${dashboardTitle || "dashboard"}_${new Date().toISOString().split("T")[0]}.pdf`; pdf.save(filename); - console.log("✅ PDF 다운로드 완료:", filename); } }; const handleDownload = async (format: "png" | "pdf") => { try { - console.log("🔍 다운로드 시작:", format); - // 실제 위젯들이 있는 캔버스 찾기 const canvas = document.querySelector(".dashboard-canvas") as HTMLElement; - console.log("🔍 캔버스 찾기:", canvas); - + if (!canvas) { alert("대시보드를 찾을 수 없습니다. 페이지를 새로고침 후 다시 시도해주세요."); return; } - console.log("📸 html-to-image 로딩 중..."); // html-to-image 동적 import - const { toPng, toJpeg } = await import("html-to-image"); + // @ts-expect-error - 동적 import + const { toPng } = await import("html-to-image"); - console.log("📸 캔버스 캡처 중..."); - // 3D/WebGL 렌더링 완료 대기 - console.log("⏳ 3D 렌더링 완료 대기 중..."); await new Promise((resolve) => setTimeout(resolve, 1000)); - + // WebGL 캔버스를 이미지로 변환 (Three.js 캔버스 보존) - console.log("🎨 WebGL 캔버스 처리 중..."); const webglCanvases = canvas.querySelectorAll("canvas"); const webglImages: { canvas: HTMLCanvasElement; dataUrl: string; rect: DOMRect }[] = []; - + webglCanvases.forEach((webglCanvas) => { try { const rect = webglCanvas.getBoundingClientRect(); const dataUrl = webglCanvas.toDataURL("image/png"); webglImages.push({ canvas: webglCanvas, dataUrl, rect }); - console.log("✅ WebGL 캔버스 캡처:", { width: rect.width, height: rect.height }); - } catch (error) { - console.warn("⚠️ WebGL 캔버스 캡처 실패:", error); + } catch { + // WebGL 캔버스 캡처 실패 시 무시 } }); - + // 캔버스의 실제 크기와 위치 가져오기 const rect = canvas.getBoundingClientRect(); const canvasWidth = canvas.scrollWidth; - + // 실제 콘텐츠의 최하단 위치 계산 const children = canvas.querySelectorAll(".canvas-element"); let maxBottom = 0; @@ -178,17 +161,9 @@ export function DashboardTopMenu({ maxBottom = relativeBottom; } }); - + // 실제 콘텐츠 높이 + 여유 공간 (50px) const canvasHeight = maxBottom > 0 ? maxBottom + 50 : canvas.scrollHeight; - - console.log("📐 캔버스 정보:", { - rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, - scroll: { width: canvasWidth, height: canvas.scrollHeight }, - calculated: { width: canvasWidth, height: canvasHeight }, - maxBottom: maxBottom, - webglCount: webglImages.length - }); // html-to-image로 캔버스 캡처 (WebGL 제외) const getDefaultBackgroundColor = () => { @@ -204,8 +179,8 @@ export function DashboardTopMenu({ pixelRatio: 2, // 고해상도 cacheBust: true, skipFonts: false, - preferredFontFormat: 'woff2', - filter: (node) => { + preferredFontFormat: "woff2", + filter: (node: Node) => { // WebGL 캔버스는 제외 (나중에 수동으로 합성) if (node instanceof HTMLCanvasElement) { return false; @@ -213,26 +188,25 @@ export function DashboardTopMenu({ return true; }, }); - + // WebGL 캔버스를 이미지 위에 합성 if (webglImages.length > 0) { - console.log("🖼️ WebGL 이미지 합성 중..."); const img = new Image(); img.src = dataUrl; await new Promise((resolve) => { img.onload = resolve; }); - + // 새 캔버스에 합성 const compositeCanvas = document.createElement("canvas"); compositeCanvas.width = img.width; compositeCanvas.height = img.height; const ctx = compositeCanvas.getContext("2d"); - + if (ctx) { // 기본 이미지 그리기 ctx.drawImage(img, 0, 0); - + // WebGL 이미지들을 위치에 맞게 그리기 for (const { dataUrl: webglDataUrl, rect: webglRect } of webglImages) { const webglImg = new Image(); @@ -240,50 +214,45 @@ export function DashboardTopMenu({ await new Promise((resolve) => { webglImg.onload = resolve; }); - + // 상대 위치 계산 (pixelRatio 2 고려) const relativeX = (webglRect.left - rect.left) * 2; const relativeY = (webglRect.top - rect.top) * 2; const width = webglRect.width * 2; const height = webglRect.height * 2; - + ctx.drawImage(webglImg, relativeX, relativeY, width, height); - console.log("✅ WebGL 이미지 합성 완료:", { x: relativeX, y: relativeY, width, height }); } - + // 합성된 이미지를 dataUrl로 변환 const compositeDataUrl = compositeCanvas.toDataURL("image/png"); - console.log("✅ 최종 합성 완료"); - + // 기존 dataUrl을 합성된 것으로 교체 return await handleDownloadWithDataUrl(compositeDataUrl, format, canvasWidth, canvasHeight); } } - console.log("✅ 캡처 완료 (WebGL 없음)"); - // WebGL이 없는 경우 기본 다운로드 await handleDownloadWithDataUrl(dataUrl, format, canvasWidth, canvasHeight); } catch (error) { - console.error("❌ 다운로드 실패:", error); alert(`다운로드에 실패했습니다.\n\n에러: ${error instanceof Error ? error.message : String(error)}`); } }; return ( -
+
{/* 좌측: 대시보드 제목 */} -
+
{dashboardTitle && (
- {dashboardTitle} - 편집 중 + {dashboardTitle} + 편집 중
)}
{/* 중앙: 해상도 선택 & 요소 추가 */} -
+
{/* 해상도 선택 */} {onResolutionChange && ( )} -
+
{/* 배경색 선택 */} {onBackgroundColorChange && ( @@ -301,7 +270,7 @@ export function DashboardTopMenu({ @@ -355,7 +324,7 @@ export function DashboardTopMenu({ )} -
+
{/* 차트 선택 */} {selectedConnection && ( -
+
커넥션: {selectedConnection.connection_name}