"use client"; import React from "react"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Save, Trash2, Palette, Download } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ElementType, ElementSubtype } from "./types"; import { ResolutionSelector, Resolution } from "./ResolutionSelector"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; interface DashboardTopMenuProps { onSaveLayout: () => void; onClearCanvas: () => void; dashboardTitle?: string; onAddElement?: (type: ElementType, subtype: ElementSubtype) => void; resolution?: Resolution; onResolutionChange?: (resolution: Resolution) => void; currentScreenResolution?: Resolution; backgroundColor?: string; onBackgroundColorChange?: (color: string) => void; } /** * 대시보드 편집 화면 상단 메뉴바 * - 차트/위젯 선택 (셀렉트박스) * - 저장/초기화 버튼 */ export function DashboardTopMenu({ onSaveLayout, onClearCanvas, dashboardTitle, onAddElement, resolution = "fhd", onResolutionChange, currentScreenResolution, backgroundColor = "#f9fafb", onBackgroundColorChange, }: DashboardTopMenuProps) { const [chartValue, setChartValue] = React.useState(""); const [widgetValue, setWidgetValue] = React.useState(""); // 차트 선택 시 캔버스 중앙에 추가 const handleChartSelect = (value: string) => { if (onAddElement) { onAddElement("chart", value as ElementSubtype); // 선택 후 즉시 리셋하여 같은 항목을 연속으로 선택 가능하게 setTimeout(() => setChartValue(""), 0); } }; // 위젯 선택 시 캔버스 중앙에 추가 const handleWidgetSelect = (value: string) => { if (onAddElement) { onAddElement("widget", value as ElementSubtype); // 선택 후 즉시 리셋하여 같은 항목을 연속으로 선택 가능하게 setTimeout(() => setWidgetValue(""), 0); } }; // 대시보드 다운로드 // 헬퍼 함수: dataUrl로 다운로드 처리 const handleDownloadWithDataUrl = async ( dataUrl: string, format: "png" | "pdf", canvasWidth: 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; 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 getDefaultBackgroundColor = () => { if (typeof window === "undefined") return "#ffffff"; const bgValue = getComputedStyle(document.documentElement).getPropertyValue("--background").trim(); return bgValue ? `hsl(${bgValue})` : "#ffffff"; }; const dataUrl = await toPng(canvas, { backgroundColor: backgroundColor || getDefaultBackgroundColor(), 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 (
{/* 좌측: 대시보드 제목 */}
{dashboardTitle && (
{dashboardTitle} 편집 중
)}
{/* 중앙: 해상도 선택 & 요소 추가 */}
{/* 해상도 선택 */} {onResolutionChange && ( )}
{/* 배경색 선택 */} {onBackgroundColorChange && (
onBackgroundColorChange(e.target.value)} className="h-10 w-20 cursor-pointer" /> onBackgroundColorChange(e.target.value)} placeholder="#f9fafb" className="flex-1" />
{[ "#ffffff", "#f9fafb", "#f3f4f6", "#e5e7eb", "#1f2937", "#111827", "#fef3c7", "#fde68a", "#dbeafe", "#bfdbfe", "#fecaca", "#fca5a5", ].map((color) => (
)}
{/* 차트 선택 */} {/* 위젯 선택 */}
{/* 우측: 액션 버튼 */}
{/* 다운로드 버튼 */} handleDownload("png")}>PNG 이미지로 저장 handleDownload("pdf")}>PDF 문서로 저장
); }