diff --git a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx index 54d61f77..5ece6a66 100644 --- a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx +++ b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx @@ -163,6 +163,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index c77cf541..cb789f43 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -466,7 +466,7 @@ export const DashboardCanvas = forwardRef( return (
+
{ + 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; + link.href = dataUrl; + 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; + await new Promise((resolve) => { + 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", + format: [imgWidth, imgHeight], + }); + + 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"); + + 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); + } + }); + + // 캔버스의 실제 크기와 위치 가져오기 + const rect = canvas.getBoundingClientRect(); + const canvasWidth = canvas.scrollWidth; + + // 실제 콘텐츠의 최하단 위치 계산 + const children = canvas.querySelectorAll(".canvas-element"); + let maxBottom = 0; + children.forEach((child) => { + const childRect = child.getBoundingClientRect(); + const relativeBottom = childRect.bottom - rect.top; + if (relativeBottom > maxBottom) { + 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 dataUrl = await toPng(canvas, { + backgroundColor: backgroundColor || "#ffffff", + width: canvasWidth, + height: canvasHeight, + pixelRatio: 2, // 고해상도 + cacheBust: true, + skipFonts: false, + preferredFontFormat: 'woff2', + filter: (node) => { + // WebGL 캔버스는 제외 (나중에 수동으로 합성) + if (node instanceof HTMLCanvasElement) { + return false; + } + 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(); + webglImg.src = webglDataUrl; + 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 (
{/* 좌측: 대시보드 제목 */} @@ -152,7 +350,7 @@ export function DashboardTopMenu({ )}
- + {/* 차트 선택 */}