-
+
+
저장 완료
대시보드가 성공적으로 저장되었습니다.
@@ -761,13 +701,13 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string {
return "달력 위젯";
case "driver-management":
return "기사 관리 위젯";
- case "list":
+ case "list-v2":
return "리스트 위젯";
- case "map-summary":
+ case "map-summary-v2":
return "커스텀 지도 카드";
case "status-summary":
return "커스텀 상태 카드";
- case "risk-alert":
+ case "risk-alert-v2":
return "리스크 알림 위젯";
case "todo":
return "할 일 위젯";
@@ -821,7 +761,7 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string {
return "calendar";
case "driver-management":
return "driver-management";
- case "list":
+ case "list-v2":
return "list-widget";
case "yard-management-3d":
return "yard-3d";
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({
)}
-
+
{/* 차트 선택 */}
{/* 우측: 액션 버튼 */}
-
-