❓
알 수 없는 위젯 타입: {element.subtype}
@@ -212,7 +212,7 @@ export function DashboardViewer({
dataUrl: string,
format: "png" | "pdf",
canvasWidth: number,
- canvasHeight: number
+ canvasHeight: number,
) => {
if (format === "png") {
console.log("💾 PNG 다운로드 시작...");
@@ -227,7 +227,7 @@ export function DashboardViewer({
} else {
console.log("📄 PDF 생성 중...");
const jsPDF = (await import("jspdf")).default;
-
+
// dataUrl에서 이미지 크기 계산
const img = new Image();
img.src = dataUrl;
@@ -274,40 +274,41 @@ export function DashboardViewer({
console.log("📸 html-to-image 로딩 중...");
// html-to-image 동적 import
+ // @ts-expect-error - html-to-image 타입 선언 누락
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,
+ console.log("✅ WebGL 캔버스 캡처:", {
+ width: rect.width,
height: rect.height,
left: rect.left,
top: rect.top,
- bottom: rect.bottom
+ bottom: rect.bottom,
});
} catch (error) {
console.warn("⚠️ WebGL 캔버스 캡처 실패:", error);
}
});
-
+
// 캔버스의 실제 크기와 위치 가져오기
const rect = canvas.getBoundingClientRect();
const canvasWidth = canvas.scrollWidth;
-
+
// 실제 콘텐츠의 최하단 위치 계산
// 뷰어 모드에서는 모든 자식 요소를 확인
const children = canvas.querySelectorAll("*");
@@ -323,17 +324,17 @@ export function DashboardViewer({
maxBottom = relativeBottom;
}
});
-
+
// 실제 콘텐츠 높이 + 여유 공간 (50px)
// maxBottom이 0이면 기본 캔버스 높이 사용
const canvasHeight = maxBottom > 50 ? maxBottom + 50 : Math.max(canvas.scrollHeight, rect.height);
-
+
console.log("📐 캔버스 정보:", {
rect: { x: rect.x, y: rect.y, left: rect.left, top: rect.top, width: rect.width, height: rect.height },
scroll: { width: canvasWidth, height: canvas.scrollHeight },
calculated: { width: canvasWidth, height: canvasHeight },
maxBottom: maxBottom,
- webglCount: webglImages.length
+ webglCount: webglImages.length,
});
// html-to-image로 캔버스 캡처 (WebGL 제외)
@@ -344,8 +345,8 @@ export function DashboardViewer({
pixelRatio: 2, // 고해상도
cacheBust: true,
skipFonts: false,
- preferredFontFormat: 'woff2',
- filter: (node) => {
+ preferredFontFormat: "woff2",
+ filter: (node: Node) => {
// WebGL 캔버스는 제외 (나중에 수동으로 합성)
if (node instanceof HTMLCanvasElement) {
return false;
@@ -353,7 +354,7 @@ export function DashboardViewer({
return true;
},
});
-
+
// WebGL 캔버스를 이미지 위에 합성
if (webglImages.length > 0) {
console.log("🖼️ WebGL 이미지 합성 중...");
@@ -362,17 +363,17 @@ export function DashboardViewer({
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();
@@ -380,28 +381,28 @@ export function DashboardViewer({
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("✅ 최종 합성 완료");
-
+
// 합성된 이미지로 다운로드
return await handleDownloadWithDataUrl(compositeDataUrl, format, canvasWidth, canvasHeight);
}
}
console.log("✅ 캡처 완료 (WebGL 없음)");
-
+
// WebGL이 없는 경우 기본 다운로드
await handleDownloadWithDataUrl(dataUrl, format, canvasWidth, canvasHeight);
} catch (error) {
@@ -409,7 +410,8 @@ export function DashboardViewer({
alert(`다운로드에 실패했습니다.\n\n에러: ${error instanceof Error ? error.message : String(error)}`);
}
},
- [backgroundColor, dashboardTitle],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [backgroundColor, dashboardTitle, handleDownloadWithDataUrl],
);
// 캔버스 설정 계산
@@ -528,11 +530,11 @@ export function DashboardViewer({
// 요소가 없는 경우
if (elements.length === 0) {
return (
-
+
📊
-
표시할 요소가 없습니다
-
대시보드 편집기에서 차트나 위젯을 추가해보세요
+
표시할 요소가 없습니다
+
대시보드 편집기에서 차트나 위젯을 추가해보세요
);
@@ -541,8 +543,8 @@ export function DashboardViewer({
return (
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
-
-
+
+
{/* 다운로드 버튼 */}
@@ -584,7 +586,7 @@ export function DashboardViewer({
{/* 태블릿 이하: 반응형 세로 정렬 */}
-
+
{/* 다운로드 버튼 */}
@@ -646,16 +648,16 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
// 태블릿 이하: 세로 스택 카드 스타일
return (
{element.showHeader !== false && (
-
{element.customTitle || element.title}
+
{element.customTitle || element.title}
{isLoading && (
-
+
-
-
업데이트 중...
+
+
업데이트 중...
)}
@@ -704,7 +706,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
return (
{element.showHeader !== false && (
-
{element.customTitle || element.title}
+
{element.customTitle || element.title}