From 68184ac49f27710179bb91fbd13d9ffa8626d963 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 12 Nov 2025 16:57:21 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard/DashboardViewer.tsx | 134 +++++++++--------- .../dashboard/widgets/MapTestWidgetV2.tsx | 49 ++++--- 2 files changed, 100 insertions(+), 83 deletions(-) diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 2b21b5f4..30a6f53c 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -16,8 +16,8 @@ import { import dynamic from "next/dynamic"; // 위젯 동적 import - 모든 위젯 -const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false }); -const MapTestWidget = dynamic(() => import("./widgets/MapTestWidget"), { ssr: false }); +// const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false }); +// const MapTestWidget = dynamic(() => import("./widgets/MapTestWidget"), { ssr: false }); const MapTestWidgetV2 = dynamic(() => import("./widgets/MapTestWidgetV2"), { ssr: false }); const ChartTestWidget = dynamic(() => import("./widgets/ChartTestWidget"), { ssr: false }); const ListTestWidget = dynamic( @@ -27,7 +27,7 @@ const ListTestWidget = dynamic( const CustomMetricTestWidget = dynamic(() => import("./widgets/CustomMetricTestWidget"), { ssr: false }); const RiskAlertTestWidget = dynamic(() => import("./widgets/RiskAlertTestWidget"), { ssr: false }); const StatusSummaryWidget = dynamic(() => import("./widgets/StatusSummaryWidget"), { ssr: false }); -const RiskAlertWidget = dynamic(() => import("./widgets/RiskAlertWidget"), { ssr: false }); +// const RiskAlertWidget = dynamic(() => import("./widgets/RiskAlertWidget"), { ssr: false }); const WeatherWidget = dynamic(() => import("./widgets/WeatherWidget"), { ssr: false }); const WeatherMapWidget = dynamic(() => import("./widgets/WeatherMapWidget"), { ssr: false }); const ExchangeWidget = dynamic(() => import("./widgets/ExchangeWidget"), { ssr: false }); @@ -51,10 +51,10 @@ const ClockWidget = dynamic( () => import("@/components/admin/dashboard/widgets/ClockWidget").then((mod) => ({ default: mod.ClockWidget })), { ssr: false }, ); -const ListWidget = dynamic( - () => import("@/components/admin/dashboard/widgets/ListWidget").then((mod) => ({ default: mod.ListWidget })), - { ssr: false }, -); +// const ListWidget = dynamic( +// () => import("@/components/admin/dashboard/widgets/ListWidget").then((mod) => ({ default: mod.ListWidget })), +// { ssr: false }, +// ); const YardManagement3DWidget = dynamic(() => import("@/components/admin/dashboard/widgets/YardManagement3DWidget"), { ssr: false, @@ -68,9 +68,9 @@ const CustomStatsWidget = dynamic(() => import("./widgets/CustomStatsWidget"), { ssr: false, }); -const CustomMetricWidget = dynamic(() => import("./widgets/CustomMetricWidget"), { - ssr: false, -}); +// const CustomMetricWidget = dynamic(() => import("./widgets/CustomMetricWidget"), { +// ssr: false, +// }); /** * 위젯 렌더링 함수 - DashboardSidebar의 모든 subtype 처리 @@ -91,10 +91,10 @@ function renderWidget(element: DashboardElement) { return ; case "clock": return ; - case "map-summary": - return ; - case "map-test": - return ; + // case "map-summary": + // return ; + // case "map-test": + // return ; case "map-summary-v2": return ; case "chart": @@ -105,14 +105,14 @@ function renderWidget(element: DashboardElement) { return ; case "risk-alert-v2": return ; - case "risk-alert": - return ; + // case "risk-alert": + // return ; case "calendar": return ; case "status-summary": return ; - case "custom-metric": - return ; + // case "custom-metric": + // return ; // === 운영/작업 지원 === case "todo": @@ -122,8 +122,8 @@ function renderWidget(element: DashboardElement) { return ; case "document": return ; - case "list": - return ; + // case "list": + // return ; case "yard-management-3d": // console.log("🏗️ 야드관리 위젯 렌더링:", { @@ -171,7 +171,7 @@ function renderWidget(element: DashboardElement) { // === 기본 fallback === default: return ( -
+
알 수 없는 위젯 타입: {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}