에러 수정

This commit is contained in:
dohyeons 2025-11-12 16:57:21 +09:00
parent cbdd9fef0f
commit 68184ac49f
2 changed files with 100 additions and 83 deletions

View File

@ -16,8 +16,8 @@ import {
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
// 위젯 동적 import - 모든 위젯 // 위젯 동적 import - 모든 위젯
const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false }); // const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false });
const MapTestWidget = dynamic(() => import("./widgets/MapTestWidget"), { ssr: false }); // const MapTestWidget = dynamic(() => import("./widgets/MapTestWidget"), { ssr: false });
const MapTestWidgetV2 = dynamic(() => import("./widgets/MapTestWidgetV2"), { ssr: false }); const MapTestWidgetV2 = dynamic(() => import("./widgets/MapTestWidgetV2"), { ssr: false });
const ChartTestWidget = dynamic(() => import("./widgets/ChartTestWidget"), { ssr: false }); const ChartTestWidget = dynamic(() => import("./widgets/ChartTestWidget"), { ssr: false });
const ListTestWidget = dynamic( const ListTestWidget = dynamic(
@ -27,7 +27,7 @@ const ListTestWidget = dynamic(
const CustomMetricTestWidget = dynamic(() => import("./widgets/CustomMetricTestWidget"), { ssr: false }); const CustomMetricTestWidget = dynamic(() => import("./widgets/CustomMetricTestWidget"), { ssr: false });
const RiskAlertTestWidget = dynamic(() => import("./widgets/RiskAlertTestWidget"), { ssr: false }); const RiskAlertTestWidget = dynamic(() => import("./widgets/RiskAlertTestWidget"), { ssr: false });
const StatusSummaryWidget = dynamic(() => import("./widgets/StatusSummaryWidget"), { 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 WeatherWidget = dynamic(() => import("./widgets/WeatherWidget"), { ssr: false });
const WeatherMapWidget = dynamic(() => import("./widgets/WeatherMapWidget"), { ssr: false }); const WeatherMapWidget = dynamic(() => import("./widgets/WeatherMapWidget"), { ssr: false });
const ExchangeWidget = dynamic(() => import("./widgets/ExchangeWidget"), { 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 })), () => import("@/components/admin/dashboard/widgets/ClockWidget").then((mod) => ({ default: mod.ClockWidget })),
{ ssr: false }, { ssr: false },
); );
const ListWidget = dynamic( // const ListWidget = dynamic(
() => import("@/components/admin/dashboard/widgets/ListWidget").then((mod) => ({ default: mod.ListWidget })), // () => import("@/components/admin/dashboard/widgets/ListWidget").then((mod) => ({ default: mod.ListWidget })),
{ ssr: false }, // { ssr: false },
); // );
const YardManagement3DWidget = dynamic(() => import("@/components/admin/dashboard/widgets/YardManagement3DWidget"), { const YardManagement3DWidget = dynamic(() => import("@/components/admin/dashboard/widgets/YardManagement3DWidget"), {
ssr: false, ssr: false,
@ -68,9 +68,9 @@ const CustomStatsWidget = dynamic(() => import("./widgets/CustomStatsWidget"), {
ssr: false, ssr: false,
}); });
const CustomMetricWidget = dynamic(() => import("./widgets/CustomMetricWidget"), { // const CustomMetricWidget = dynamic(() => import("./widgets/CustomMetricWidget"), {
ssr: false, // ssr: false,
}); // });
/** /**
* - DashboardSidebar의 subtype * - DashboardSidebar의 subtype
@ -91,10 +91,10 @@ function renderWidget(element: DashboardElement) {
return <CalculatorWidget element={element} />; return <CalculatorWidget element={element} />;
case "clock": case "clock":
return <ClockWidget element={element} />; return <ClockWidget element={element} />;
case "map-summary": // case "map-summary":
return <MapSummaryWidget element={element} />; // return <MapSummaryWidget element={element} />;
case "map-test": // case "map-test":
return <MapTestWidget element={element} />; // return <MapTestWidget element={element} />;
case "map-summary-v2": case "map-summary-v2":
return <MapTestWidgetV2 element={element} />; return <MapTestWidgetV2 element={element} />;
case "chart": case "chart":
@ -105,14 +105,14 @@ function renderWidget(element: DashboardElement) {
return <CustomMetricTestWidget element={element} />; return <CustomMetricTestWidget element={element} />;
case "risk-alert-v2": case "risk-alert-v2":
return <RiskAlertTestWidget element={element} />; return <RiskAlertTestWidget element={element} />;
case "risk-alert": // case "risk-alert":
return <RiskAlertWidget element={element} />; // return <RiskAlertWidget element={element} />;
case "calendar": case "calendar":
return <CalendarWidget element={element} />; return <CalendarWidget element={element} />;
case "status-summary": case "status-summary":
return <StatusSummaryWidget element={element} />; return <StatusSummaryWidget element={element} />;
case "custom-metric": // case "custom-metric":
return <CustomMetricWidget element={element} />; // return <CustomMetricWidget element={element} />;
// === 운영/작업 지원 === // === 운영/작업 지원 ===
case "todo": case "todo":
@ -122,8 +122,8 @@ function renderWidget(element: DashboardElement) {
return <BookingAlertWidget element={element} />; return <BookingAlertWidget element={element} />;
case "document": case "document":
return <DocumentWidget element={element} />; return <DocumentWidget element={element} />;
case "list": // case "list":
return <ListWidget element={element} />; // return <ListWidget element={element} />;
case "yard-management-3d": case "yard-management-3d":
// console.log("🏗️ 야드관리 위젯 렌더링:", { // console.log("🏗️ 야드관리 위젯 렌더링:", {
@ -171,7 +171,7 @@ function renderWidget(element: DashboardElement) {
// === 기본 fallback === // === 기본 fallback ===
default: default:
return ( return (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-muted to-muted-foreground p-4 text-white"> <div className="from-muted to-muted-foreground flex h-full w-full items-center justify-center bg-gradient-to-br p-4 text-white">
<div className="text-center"> <div className="text-center">
<div className="mb-2 text-3xl"></div> <div className="mb-2 text-3xl"></div>
<div className="text-sm"> : {element.subtype}</div> <div className="text-sm"> : {element.subtype}</div>
@ -212,7 +212,7 @@ export function DashboardViewer({
dataUrl: string, dataUrl: string,
format: "png" | "pdf", format: "png" | "pdf",
canvasWidth: number, canvasWidth: number,
canvasHeight: number canvasHeight: number,
) => { ) => {
if (format === "png") { if (format === "png") {
console.log("💾 PNG 다운로드 시작..."); console.log("💾 PNG 다운로드 시작...");
@ -274,6 +274,7 @@ export function DashboardViewer({
console.log("📸 html-to-image 로딩 중..."); console.log("📸 html-to-image 로딩 중...");
// html-to-image 동적 import // html-to-image 동적 import
// @ts-expect-error - html-to-image 타입 선언 누락
const { toPng } = await import("html-to-image"); const { toPng } = await import("html-to-image");
console.log("📸 캔버스 캡처 중..."); console.log("📸 캔버스 캡처 중...");
@ -297,7 +298,7 @@ export function DashboardViewer({
height: rect.height, height: rect.height,
left: rect.left, left: rect.left,
top: rect.top, top: rect.top,
bottom: rect.bottom bottom: rect.bottom,
}); });
} catch (error) { } catch (error) {
console.warn("⚠️ WebGL 캔버스 캡처 실패:", error); console.warn("⚠️ WebGL 캔버스 캡처 실패:", error);
@ -333,7 +334,7 @@ export function DashboardViewer({
scroll: { width: canvasWidth, height: canvas.scrollHeight }, scroll: { width: canvasWidth, height: canvas.scrollHeight },
calculated: { width: canvasWidth, height: canvasHeight }, calculated: { width: canvasWidth, height: canvasHeight },
maxBottom: maxBottom, maxBottom: maxBottom,
webglCount: webglImages.length webglCount: webglImages.length,
}); });
// html-to-image로 캔버스 캡처 (WebGL 제외) // html-to-image로 캔버스 캡처 (WebGL 제외)
@ -344,8 +345,8 @@ export function DashboardViewer({
pixelRatio: 2, // 고해상도 pixelRatio: 2, // 고해상도
cacheBust: true, cacheBust: true,
skipFonts: false, skipFonts: false,
preferredFontFormat: 'woff2', preferredFontFormat: "woff2",
filter: (node) => { filter: (node: Node) => {
// WebGL 캔버스는 제외 (나중에 수동으로 합성) // WebGL 캔버스는 제외 (나중에 수동으로 합성)
if (node instanceof HTMLCanvasElement) { if (node instanceof HTMLCanvasElement) {
return false; return false;
@ -409,7 +410,8 @@ export function DashboardViewer({
alert(`다운로드에 실패했습니다.\n\n에러: ${error instanceof Error ? error.message : String(error)}`); 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) { if (elements.length === 0) {
return ( return (
<div className="flex h-full items-center justify-center bg-muted"> <div className="bg-muted flex h-full items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="mb-4 text-6xl">📊</div> <div className="mb-4 text-6xl">📊</div>
<div className="mb-2 text-xl font-medium text-foreground"> </div> <div className="text-foreground mb-2 text-xl font-medium"> </div>
<div className="text-sm text-muted-foreground"> </div> <div className="text-muted-foreground text-sm"> </div>
</div> </div>
</div> </div>
); );
@ -541,8 +543,8 @@ export function DashboardViewer({
return ( return (
<DashboardProvider> <DashboardProvider>
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */} {/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
<div className="hidden min-h-screen bg-muted py-8 lg:block" style={{ backgroundColor }}> <div className="bg-muted hidden min-h-screen py-8 lg:block" style={{ backgroundColor }}>
<div className="mx-auto px-4" style={{ width: '100%', maxWidth: 'none' }}> <div className="mx-auto px-4" style={{ width: "100%", maxWidth: "none" }}>
{/* 다운로드 버튼 */} {/* 다운로드 버튼 */}
<div className="mb-4 flex justify-end"> <div className="mb-4 flex justify-end">
<DropdownMenu> <DropdownMenu>
@ -584,7 +586,7 @@ export function DashboardViewer({
</div> </div>
{/* 태블릿 이하: 반응형 세로 정렬 */} {/* 태블릿 이하: 반응형 세로 정렬 */}
<div className="block min-h-screen bg-muted p-4 lg:hidden" style={{ backgroundColor }}> <div className="bg-muted block min-h-screen p-4 lg:hidden" style={{ backgroundColor }}>
<div className="mx-auto max-w-3xl space-y-4"> <div className="mx-auto max-w-3xl space-y-4">
{/* 다운로드 버튼 */} {/* 다운로드 버튼 */}
<div className="flex justify-end"> <div className="flex justify-end">
@ -646,16 +648,16 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
// 태블릿 이하: 세로 스택 카드 스타일 // 태블릿 이하: 세로 스택 카드 스타일
return ( return (
<div <div
className="relative overflow-hidden rounded-lg border border-border bg-background shadow-sm" className="border-border bg-background relative overflow-hidden rounded-lg border shadow-sm"
style={{ minHeight: "300px" }} style={{ minHeight: "300px" }}
> >
{element.showHeader !== false && ( {element.showHeader !== false && (
<div className="flex items-center justify-between px-2 py-1"> <div className="flex items-center justify-between px-2 py-1">
<h3 className="text-xs font-semibold text-foreground">{element.customTitle || element.title}</h3> <h3 className="text-foreground text-xs font-semibold">{element.customTitle || element.title}</h3>
<button <button
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} disabled={isLoading}
className="text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50" className="text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
title="새로고침" title="새로고침"
> >
<svg <svg
@ -677,7 +679,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
<div className={element.showHeader !== false ? "p-2" : "p-2"} style={{ minHeight: "250px" }}> <div className={element.showHeader !== false ? "p-2" : "p-2"} style={{ minHeight: "250px" }}>
{!isMounted ? ( {!isMounted ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
</div> </div>
) : element.type === "chart" ? ( ) : element.type === "chart" ? (
<ChartRenderer element={element} data={data} width={undefined} height={250} /> <ChartRenderer element={element} data={data} width={undefined} height={250} />
@ -686,10 +688,10 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
)} )}
</div> </div>
{isLoading && ( {isLoading && (
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-background"> <div className="bg-opacity-75 bg-background absolute inset-0 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="border-primary mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
<div className="text-sm text-foreground"> ...</div> <div className="text-foreground text-sm"> ...</div>
</div> </div>
</div> </div>
)} )}
@ -704,7 +706,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
return ( return (
<div <div
className="absolute overflow-hidden rounded-lg border border-border bg-background shadow-sm" className="border-border bg-background absolute overflow-hidden rounded-lg border shadow-sm"
style={{ style={{
left: `${leftPercentage}%`, left: `${leftPercentage}%`,
top: element.position.y, top: element.position.y,
@ -714,11 +716,11 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
> >
{element.showHeader !== false && ( {element.showHeader !== false && (
<div className="flex items-center justify-between px-2 py-1"> <div className="flex items-center justify-between px-2 py-1">
<h3 className="text-xs font-semibold text-foreground">{element.customTitle || element.title}</h3> <h3 className="text-foreground text-xs font-semibold">{element.customTitle || element.title}</h3>
<button <button
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} disabled={isLoading}
className="text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50" className="text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
title="새로고침" title="새로고침"
> >
<svg <svg
@ -740,7 +742,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
<div className={element.showHeader !== false ? "h-[calc(100%-32px)] w-full" : "h-full w-full"}> <div className={element.showHeader !== false ? "h-[calc(100%-32px)] w-full" : "h-full w-full"}>
{!isMounted ? ( {!isMounted ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
</div> </div>
) : element.type === "chart" ? ( ) : element.type === "chart" ? (
<ChartRenderer <ChartRenderer
@ -754,10 +756,10 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
)} )}
</div> </div>
{isLoading && ( {isLoading && (
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-background"> <div className="bg-opacity-75 bg-background absolute inset-0 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="border-primary mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
<div className="text-sm text-foreground"> ...</div> <div className="text-foreground text-sm"> ...</div>
</div> </div>
</div> </div>
)} )}

View File

@ -65,6 +65,10 @@ interface PolygonData {
} }
export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
console.log("🗺️ MapTestWidgetV2 컴포넌트 마운트/렌더링");
console.log("📦 element:", element);
console.log("📊 dataSources:", element?.dataSources);
const [markers, setMarkers] = useState<MarkerData[]>([]); const [markers, setMarkers] = useState<MarkerData[]>([]);
const [prevMarkers, setPrevMarkers] = useState<MarkerData[]>([]); // 이전 마커 위치 저장 const [prevMarkers, setPrevMarkers] = useState<MarkerData[]>([]); // 이전 마커 위치 저장
const [polygons, setPolygons] = useState<PolygonData[]>([]); const [polygons, setPolygons] = useState<PolygonData[]>([]);
@ -73,9 +77,6 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const [geoJsonData, setGeoJsonData] = useState<any>(null); const [geoJsonData, setGeoJsonData] = useState<any>(null);
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null); const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
// // console.log("🧪 MapTestWidgetV2 렌더링!", element);
// // console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length);
// dataSources를 useMemo로 추출 (circular reference 방지) // dataSources를 useMemo로 추출 (circular reference 방지)
const dataSources = useMemo(() => { const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources; return element?.dataSources || element?.chartConfig?.dataSources;
@ -908,36 +909,37 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
loadGeoJsonData(); loadGeoJsonData();
}, []); }, []);
// 초기 로드 및 자동 새로고침 // 초기 로드 및 자동 새로고침 (마커 데이터만 polling)
useEffect(() => { useEffect(() => {
console.log("🔄 지도 위젯 초기화"); console.log("🔄 지도 위젯 초기화 useEffect 실행됨!");
console.log("📊 dataSources 상태:", dataSources);
if (!dataSources || dataSources.length === 0) { if (!dataSources || dataSources.length === 0) {
console.log("⚠️ dataSources가 없거나 비어있음"); console.log("⚠️ dataSources가 없거나 비어있음 - polling 시작 안함");
setMarkers([]); setMarkers([]);
setPolygons([]); setPolygons([]);
return; return;
} }
// 즉시 첫 로드 // 즉시 첫 로드 (마커 데이터)
console.log("📡 초기 데이터 로드"); console.log("📡 초기 마커 데이터 로드");
loadMultipleDataSources(); loadMultipleDataSources();
// 5초마다 자동 새로고침 // 5초마다 마커 데이터만 자동 새로고침 (지도 타일은 안 건드림)
const refreshInterval = 5; const refreshInterval = 5;
console.log(`⏱️ 자동 새로고침 설정: ${refreshInterval}초마다`); console.log(`⏱️ 마커 polling 시작: ${refreshInterval}초마다`);
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행"); console.log("🔄 마커 자동 새로고침 실행");
loadMultipleDataSources(); loadMultipleDataSources();
}, refreshInterval * 1000); }, refreshInterval * 1000);
return () => { return () => {
console.log("⏹️ 자동 새로고침 정리"); console.log("⏹️ 마커 polling 정리");
clearInterval(intervalId); clearInterval(intervalId);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [dataSources]);
// 타일맵 URL (chartConfig에서 가져오기) // 타일맵 URL (chartConfig에서 가져오기)
const tileMapUrl = const tileMapUrl =
@ -986,10 +988,6 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<p className="text-destructive text-sm">{error}</p> <p className="text-destructive text-sm">{error}</p>
</div> </div>
) : !element?.dataSources || element.dataSources.length === 0 ? (
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
) : ( ) : (
<MapContainer center={center} zoom={13} style={{ width: "100%", height: "100%" }} className="z-0"> <MapContainer center={center} zoom={13} style={{ width: "100%", height: "100%" }} className="z-0">
<TileLayer url={tileMapUrl} attribution="&copy; VWorld" maxZoom={19} /> <TileLayer url={tileMapUrl} attribution="&copy; VWorld" maxZoom={19} />
@ -1244,6 +1242,23 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
</Marker> </Marker>
); );
})} })}
{/* 데이터 소스 없을 때 안내 메시지 */}
{(!element?.dataSources || element.dataSources.length === 0) && (
<div
className="pointer-events-none absolute left-1/2 top-1/2 z-[1000] -translate-x-1/2 -translate-y-1/2"
style={{ zIndex: 1000 }}
>
<div className="rounded-lg border-2 border-dashed border-primary bg-background/95 p-4 shadow-lg backdrop-blur-sm">
<p className="text-center text-sm font-medium">
📍
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
</p>
</div>
</div>
)}
</MapContainer> </MapContainer>
)} )}
</div> </div>