diff --git a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx index 54d61f77..5ece6a66 100644 --- a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx +++ b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx @@ -163,6 +163,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index c77cf541..cb789f43 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -466,7 +466,7 @@ export const DashboardCanvas = forwardRef( return (
+
{ + 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 dataUrl = await toPng(canvas, { + backgroundColor: backgroundColor || "#ffffff", + 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 (
{/* 좌측: 대시보드 제목 */} @@ -152,7 +350,7 @@ export function DashboardTopMenu({ )}
- + {/* 차트 선택 */}
diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx index 7e9cddec..b42ad4f5 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx @@ -647,6 +647,7 @@ export default function Yard3DCanvas({ fov: 50, }} shadows + gl={{ preserveDrawingBuffer: true }} > >({}); const [loadingElements, setLoadingElements] = useState>(new Set()); + // 대시보드 다운로드 + // 헬퍼 함수: 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 = useCallback( + async (format: "png" | "pdf") => { + try { + console.log("🔍 다운로드 시작:", format); + + const canvas = document.querySelector(".dashboard-viewer-canvas") as HTMLElement; + console.log("🔍 캔버스 찾기:", canvas); + + if (!canvas) { + alert("대시보드를 찾을 수 없습니다. 페이지를 새로고침 후 다시 시도해주세요."); + return; + } + + console.log("📸 html-to-image 로딩 중..."); + // html-to-image 동적 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, + left: rect.left, + top: rect.top, + bottom: rect.bottom + }); + } catch (error) { + console.warn("⚠️ WebGL 캔버스 캡처 실패:", error); + } + }); + + // 캔버스의 실제 크기와 위치 가져오기 + const rect = canvas.getBoundingClientRect(); + const canvasWidth = canvas.scrollWidth; + + // 실제 콘텐츠의 최하단 위치 계산 + // 뷰어 모드에서는 모든 자식 요소를 확인 + const children = canvas.querySelectorAll("*"); + let maxBottom = 0; + children.forEach((child) => { + // canvas 자신이나 너무 작은 요소는 제외 + if (child === canvas || child.clientHeight < 10) { + return; + } + const childRect = child.getBoundingClientRect(); + const relativeBottom = childRect.bottom - rect.top; + if (relativeBottom > maxBottom) { + 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 + }); + + // html-to-image로 캔버스 캡처 (WebGL 제외) + const dataUrl = await toPng(canvas, { + backgroundColor: backgroundColor || "#ffffff", + 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("✅ 최종 합성 완료"); + + // 합성된 이미지로 다운로드 + 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)}`); + } + }, + [backgroundColor, dashboardTitle], + ); + // 캔버스 설정 계산 const canvasConfig = useMemo(() => { return RESOLUTIONS[resolution as Resolution] || RESOLUTIONS.fhd; @@ -327,8 +543,24 @@ export function DashboardViewer({ {/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
+ {/* 다운로드 버튼 */} +
+ + + + + + handleDownload("png")}>PNG 이미지로 저장 + handleDownload("pdf")}>PDF 문서로 저장 + + +
+
- {sortedElements.map((element) => ( - loadElementData(element)} - isMobile={true} - /> - ))} + {/* 다운로드 버튼 */} +
+ + + + + + handleDownload("png")}>PNG 이미지로 저장 + handleDownload("pdf")}>PDF 문서로 저장 + + +
+ +
+ {sortedElements.map((element) => ( + loadElementData(element)} + isMobile={true} + /> + ))} +
diff --git a/frontend/components/dashboard/widgets/ChartTestWidget.tsx b/frontend/components/dashboard/widgets/ChartTestWidget.tsx index 22d7f2db..ce8c119c 100644 --- a/frontend/components/dashboard/widgets/ChartTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ChartTestWidget.tsx @@ -22,7 +22,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { const containerRef = useRef(null); const [containerSize, setContainerSize] = useState({ width: 600, height: 400 }); - console.log("🧪 ChartTestWidget 렌더링 (D3 기반)!", element); + // console.log("🧪 ChartTestWidget 렌더링 (D3 기반)!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; @@ -48,11 +48,11 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { const dataSources = element?.dataSources || element?.chartConfig?.dataSources; if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setLoading(true); setError(null); @@ -60,7 +60,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { const results = await Promise.allSettled( dataSources.map(async (source) => { try { - console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); + // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); @@ -87,7 +87,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { } }); - console.log(`✅ 총 ${allData.length}개의 데이터 로딩 완료`); + // console.log(`✅ 총 ${allData.length}개의 데이터 로딩 완료`); setData(allData); setLastRefreshTime(new Date()); } catch (err: any) { @@ -100,7 +100,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { // 수동 새로고침 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); @@ -212,15 +212,15 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); @@ -241,14 +241,20 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { // 병합 모드: 여러 데이터 소스를 하나로 합침 if (mergeMode && dataSourceConfigs.length > 1) { + // console.log("🔀 병합 모드 활성화"); + + // 모든 데이터 소스의 X축 필드 수집 (첫 번째 데이터 소스의 X축 사용) const baseConfig = dataSourceConfigs[0]; const xAxisField = baseConfig.xAxis; - const yAxisField = baseConfig.yAxis[0]; + + // console.log("📊 X축 필드:", xAxisField); - // X축 값 수집 - dataSourceConfigs.forEach((dsConfig) => { + // X축 값 수집 (모든 데이터 소스에서) + dataSourceConfigs.forEach((dsConfig, idx) => { const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name; const sourceData = data.filter((item) => item._source === sourceName); + // console.log(` 소스 ${idx + 1} (${sourceName}): ${sourceData.length}개 행`); + sourceData.forEach((item) => { if (item[xAxisField] !== undefined) { labels.add(String(item[xAxisField])); @@ -256,26 +262,36 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { }); }); - // 데이터 병합 - const mergedData: number[] = []; - labels.forEach((label) => { - let totalValue = 0; - dataSourceConfigs.forEach((dsConfig) => { - const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name; - const sourceData = data.filter((item) => item._source === sourceName); - const matchingItem = sourceData.find((item) => String(item[xAxisField]) === label); - if (matchingItem && yAxisField) { - totalValue += parseFloat(matchingItem[yAxisField]) || 0; - } + // console.log("📍 수집된 X축 라벨:", Array.from(labels)); + + // 각 데이터 소스별로 데이터셋 생성 (병합하지 않고 각각 표시) + dataSourceConfigs.forEach((dsConfig, idx) => { + const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name || `소스 ${idx + 1}`; + const sourceData = data.filter((item) => item._source === sourceName); + + // 각 Y축 필드마다 데이터셋 생성 + (dsConfig.yAxis || []).forEach((yAxisField, yIdx) => { + const datasetData: number[] = []; + + labels.forEach((label) => { + const matchingItem = sourceData.find((item) => String(item[xAxisField]) === label); + const value = matchingItem && yAxisField ? parseFloat(matchingItem[yAxisField]) || 0 : 0; + datasetData.push(value); + }); + + // console.log(` 📈 ${sourceName} - ${yAxisField}: [${datasetData.join(", ")}]`); + + datasets.push({ + label: `${sourceName} - ${yAxisField}`, + data: datasetData, + backgroundColor: COLORS[(idx * 2 + yIdx) % COLORS.length], + borderColor: COLORS[(idx * 2 + yIdx) % COLORS.length], + type: dsConfig.chartType || chartType, // 데이터 소스별 차트 타입 + }); }); - mergedData.push(totalValue); }); - datasets.push({ - label: yAxisField, - data: mergedData, - color: COLORS[0], - }); + // console.log("✅ 병합 모드 데이터셋 생성 완료:", datasets.length, "개"); } else { // 일반 모드: 각 데이터 소스를 별도로 표시 dataSourceConfigs.forEach((dsConfig, index) => { diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx index 48a8c161..f2e6ebd7 100644 --- a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx @@ -64,7 +64,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const [selectedMetric, setSelectedMetric] = useState(null); const [isDetailOpen, setIsDetailOpen] = useState(false); - console.log("🧪 CustomMetricTestWidget 렌더링!", element); + // console.log("🧪 CustomMetricTestWidget 렌더링!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; @@ -101,10 +101,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(groupByDS.query); - if (result.success && result.data?.rows) { - const rows = result.data.rows; + if (result && result.rows) { + const rows = result.rows; if (rows.length > 0) { - const columns = result.data.columns || Object.keys(rows[0]); + const columns = result.columns || Object.keys(rows[0]); const labelColumn = columns[0]; const valueColumn = columns[1]; @@ -121,12 +121,17 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg else if (dataSourceType === "api") { if (!groupByDS.endpoint) return; - const { dashboardApi } = await import("@/lib/api/dashboard"); - const result = await dashboardApi.fetchExternalApi({ - method: "GET", - url: groupByDS.endpoint, - headers: (groupByDS as any).headers || {}, + // REST API 호출 (백엔드 프록시 사용) + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: groupByDS.endpoint, + method: "GET", + headers: (groupByDS as any).headers || {}, + }), }); + const result = await response.json(); if (result.success && result.data) { let rows: any[] = []; @@ -163,11 +168,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const dataSources = element?.dataSources || element?.chartConfig?.dataSources; if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setLoading(true); setError(null); @@ -176,7 +181,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const results = await Promise.allSettled( dataSources.map(async (source, sourceIndex) => { try { - console.log(`📡 데이터 소스 ${sourceIndex + 1} "${source.name || source.id}" 로딩 중...`); + // console.log(`📡 데이터 소스 ${sourceIndex + 1} "${source.name || source.id}" 로딩 중...`); let rows: any[] = []; if (source.type === "api") { @@ -185,7 +190,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg rows = await loadDatabaseData(source); } - console.log(`✅ 데이터 소스 ${sourceIndex + 1}: ${rows.length}개 행`); + // console.log(`✅ 데이터 소스 ${sourceIndex + 1}: ${rows.length}개 행`); return { sourceName: source.name || `데이터 소스 ${sourceIndex + 1}`, @@ -203,7 +208,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg }), ); - console.log(`✅ 총 ${results.length}개의 데이터 소스 로딩 완료`); + // console.log(`✅ 총 ${results.length}개의 데이터 소스 로딩 완료`); // 각 데이터 소스별로 메트릭 생성 const allMetrics: any[] = []; @@ -235,11 +240,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg return typeof value === "string" || !numericColumns.includes(col); }); - console.log(`📊 [${sourceName}] 컬럼 분석:`, { - 전체: columns, - 숫자: numericColumns, - 문자열: stringColumns, - }); + // console.log(`📊 [${sourceName}] 컬럼 분석:`, { + // 전체: columns, + // 숫자: numericColumns, + // 문자열: stringColumns, + // }); // 🆕 자동 집계 로직: 집계 컬럼 이름으로 판단 (count, 개수, sum, avg 등) const isAggregated = numericColumns.some((col) => @@ -248,7 +253,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (isAggregated && numericColumns.length > 0) { // 집계 컬럼이 있으면 이미 집계된 데이터로 판단 (GROUP BY 결과) - console.log(`✅ [${sourceName}] 집계된 데이터로 판단 (집계 컬럼 발견: ${numericColumns.join(", ")})`); + // console.log(`✅ [${sourceName}] 집계된 데이터로 판단 (집계 컬럼 발견: ${numericColumns.join(", ")})`); rows.forEach((row, index) => { // 라벨: 첫 번째 문자열 컬럼 @@ -259,7 +264,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const valueField = numericColumns[0]; const value = Number(row[valueField]) || 0; - console.log(` [${sourceName}] 메트릭: ${label} = ${value}`); + // console.log(` [${sourceName}] 메트릭: ${label} = ${value}`); allMetrics.push({ label: label, @@ -273,11 +278,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg }); } else { // 숫자 컬럼이 없으면 자동 집계 (마지막 컬럼 기준) - console.log(`✅ [${sourceName}] 자동 집계 모드 (숫자 컬럼 없음)`); + // console.log(`✅ [${sourceName}] 자동 집계 모드 (숫자 컬럼 없음)`); // 마지막 컬럼을 집계 기준으로 사용 const aggregateField = columns[columns.length - 1]; - console.log(` [${sourceName}] 집계 기준 컬럼: ${aggregateField}`); + // console.log(` [${sourceName}] 집계 기준 컬럼: ${aggregateField}`); // 해당 컬럼의 값별로 카운트 const countMap = new Map(); @@ -288,7 +293,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 카운트 결과를 메트릭으로 변환 countMap.forEach((count, label) => { - console.log(` [${sourceName}] 자동 집계: ${label} = ${count}개`); + // console.log(` [${sourceName}] 자동 집계: ${label} = ${count}개`); allMetrics.push({ label: label, @@ -314,20 +319,20 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } // 🆕 숫자 컬럼이 없을 때의 기존 로직은 주석 처리 - if (false) { + /* if (false && result.status === "fulfilled") { // 숫자 컬럼이 없으면 각 컬럼별 고유값 개수 표시 - console.log(`📊 [${sourceName}] 문자열 데이터, 각 컬럼별 고유값 개수 표시`); + // console.log(`📊 [${sourceName}] 문자열 데이터, 각 컬럼별 고유값 개수 표시`); // 데이터 소스에서 선택된 컬럼 가져오기 const dataSourceConfig = (element?.dataSources || element?.chartConfig?.dataSources)?.find( - (ds) => ds.name === sourceName || ds.id === result.value.sourceIndex.toString(), + (ds) => ds.name === sourceName || ds.id === result.value?.sourceIndex.toString(), ); const selectedColumns = dataSourceConfig?.selectedColumns || []; // 선택된 컬럼이 있으면 해당 컬럼만, 없으면 전체 컬럼 표시 const columnsToShow = selectedColumns.length > 0 ? selectedColumns : columns; - console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); + // console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); columnsToShow.forEach((col) => { // 해당 컬럼이 실제로 존재하는지 확인 @@ -340,7 +345,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const uniqueValues = new Set(rows.map((row) => row[col])); const uniqueCount = uniqueValues.size; - console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); + // console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); allMetrics.push({ label: `${sourceName} - ${col} (고유값)`, @@ -363,24 +368,24 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg sourceName: sourceName, rawData: rows, // 원본 데이터 저장 }); - } + } */ } else { // 행이 많으면 각 컬럼별 고유값 개수 + 총 개수 표시 - console.log(`📊 [${sourceName}] 일반 데이터 (행 많음), 컬럼별 통계 표시`); + // console.log(`📊 [${sourceName}] 일반 데이터 (행 많음), 컬럼별 통계 표시`); const firstRow = rows[0]; const columns = Object.keys(firstRow); // 데이터 소스에서 선택된 컬럼 가져오기 const dataSourceConfig = (element?.dataSources || element?.chartConfig?.dataSources)?.find( - (ds) => ds.name === sourceName || ds.id === result.value.sourceIndex.toString(), + (ds) => ds.name === sourceName || (result.status === "fulfilled" && ds.id === result.value?.sourceIndex.toString()), ); const selectedColumns = dataSourceConfig?.selectedColumns || []; // 선택된 컬럼이 있으면 해당 컬럼만, 없으면 전체 컬럼 표시 const columnsToShow = selectedColumns.length > 0 ? selectedColumns : columns; - console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); + // console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); // 각 컬럼별 고유값 개수 columnsToShow.forEach((col) => { @@ -393,7 +398,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const uniqueValues = new Set(rows.map((row) => row[col])); const uniqueCount = uniqueValues.size; - console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); + // console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); allMetrics.push({ label: `${sourceName} - ${col} (고유값)`, @@ -419,7 +424,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } }); - console.log(`✅ 총 ${allMetrics.length}개의 메트릭 생성 완료`); + // console.log(`✅ 총 ${allMetrics.length}개의 메트릭 생성 완료`); setMetrics(allMetrics); setLastRefreshTime(new Date()); } catch (err) { @@ -460,13 +465,13 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadAllData(); }, [loadAllData]); // XML 데이터 파싱 const parseXmlData = (xmlText: string): any[] => { - console.log("🔍 XML 파싱 시작"); + // console.log("🔍 XML 파싱 시작"); try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); @@ -486,7 +491,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg result.push(obj); } - console.log(`✅ XML 파싱 완료: ${result.length}개 레코드`); + // console.log(`✅ XML 파싱 완료: ${result.length}개 레코드`); return result; } catch (error) { console.error("❌ XML 파싱 실패:", error); @@ -496,16 +501,16 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 텍스트/CSV 데이터 파싱 const parseTextData = (text: string): any[] => { - console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500)); + // console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500)); // XML 감지 if (text.trim().startsWith("")) { - console.log("📄 XML 형식 감지"); + // console.log("📄 XML 형식 감지"); return parseXmlData(text); } // CSV 파싱 - console.log("📄 CSV 형식으로 파싱 시도"); + // console.log("📄 CSV 형식으로 파싱 시도"); const lines = text.trim().split("\n"); if (lines.length === 0) return []; @@ -523,7 +528,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg result.push(obj); } - console.log(`✅ CSV 파싱 완료: ${result.length}개 행`); + // console.log(`✅ CSV 파싱 완료: ${result.length}개 행`); return result; }; @@ -552,7 +557,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } } - console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params)); + // console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params)); const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", @@ -578,7 +583,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } const result = await response.json(); - console.log("✅ API 응답:", result); + // console.log("✅ API 응답:", result); if (!result.success) { console.error("❌ API 실패:", result); @@ -589,10 +594,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 텍스트/XML 데이터 처리 if (typeof processedData === "string") { - console.log("📄 텍스트 형식 데이터 감지"); + // console.log("📄 텍스트 형식 데이터 감지"); processedData = parseTextData(processedData); } else if (processedData && typeof processedData === "object" && processedData.text) { - console.log("📄 래핑된 텍스트 데이터 감지"); + // console.log("📄 래핑된 텍스트 데이터 감지"); processedData = parseTextData(processedData.text); } @@ -608,12 +613,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } } else if (!Array.isArray(processedData) && typeof processedData === "object") { // JSON Path 없으면 자동으로 배열 찾기 - console.log("🔍 JSON Path 없음, 자동으로 배열 찾기 시도"); + // console.log("🔍 JSON Path 없음, 자동으로 배열 찾기 시도"); const arrayKeys = ["data", "items", "result", "records", "rows", "list"]; for (const key of arrayKeys) { if (Array.isArray(processedData[key])) { - console.log(`✅ 배열 발견: ${key}`); + // console.log(`✅ 배열 발견: ${key}`); processedData = processedData[key]; break; } @@ -681,15 +686,15 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadAllData(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadAllData]); diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index d10ee6be..64248532 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -34,13 +34,13 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const [currentPage, setCurrentPage] = useState(1); const [lastRefreshTime, setLastRefreshTime] = useState(null); - // console.log("🧪 ListTestWidget 렌더링!", element); + // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; }, [element?.dataSources, element?.chartConfig?.dataSources]); - // console.log("📊 dataSources 확인:", { + // // console.log("📊 dataSources 확인:", { // hasDataSources: !!dataSources, // dataSourcesLength: dataSources?.length || 0, // dataSources: dataSources, @@ -61,11 +61,11 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setIsLoading(true); setError(null); @@ -74,7 +74,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const results = await Promise.allSettled( dataSources.map(async (source) => { try { - console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); + // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); @@ -127,7 +127,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { }); setLastRefreshTime(new Date()); - console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`); + // console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`); } catch (err) { setError(err instanceof Error ? err.message : "데이터 로딩 실패"); } finally { @@ -137,7 +137,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); @@ -161,6 +161,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { headers: { "Content-Type": "application/json", }, + credentials: "include", body: JSON.stringify({ url: source.endpoint, method: "GET", @@ -180,7 +181,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { } const result = await response.json(); - console.log("✅ API 응답:", result); + // console.log("✅ API 응답:", result); if (!result.success) { console.error("❌ API 실패:", result); @@ -247,7 +248,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(source.query); - // console.log("💾 내부 DB 쿼리 결과:", { + // // console.log("💾 내부 DB 쿼리 결과:", { // hasRows: !!result.rows, // rowCount: result.rows?.length || 0, // hasColumns: !!result.columns, @@ -260,7 +261,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const mappedRows = applyColumnMapping(result.rows, source.columnMapping); const columns = mappedRows.length > 0 ? Object.keys(mappedRows[0]) : result.columns; - // console.log("✅ 매핑 후:", { + // // console.log("✅ 매핑 후:", { // columns, // rowCount: mappedRows.length, // firstMappedRow: mappedRows[0], @@ -291,15 +292,15 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index cec70b7a..5eeeca12 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -67,8 +67,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const [geoJsonData, setGeoJsonData] = useState(null); const [lastRefreshTime, setLastRefreshTime] = useState(null); - console.log("🧪 MapTestWidgetV2 렌더링!", element); - console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length); + // // console.log("🧪 MapTestWidgetV2 렌더링!", element); + // // console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length); // dataSources를 useMemo로 추출 (circular reference 방지) const dataSources = useMemo(() => { @@ -80,11 +80,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const dataSourcesList = dataSources; if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setLoading(true); setError(null); @@ -93,7 +93,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const results = await Promise.allSettled( dataSources.map(async (source) => { try { - console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); + // // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); @@ -114,21 +114,21 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const allPolygons: PolygonData[] = []; results.forEach((result, index) => { - console.log(`🔍 결과 ${index}:`, result); + // // console.log(`🔍 결과 ${index}:`, result); if (result.status === "fulfilled" && result.value) { const value = result.value as { markers: MarkerData[]; polygons: PolygonData[] }; - console.log(`✅ 데이터 소스 ${index} 성공:`, value); + // // console.log(`✅ 데이터 소스 ${index} 성공:`, value); // 마커 병합 if (value.markers && Array.isArray(value.markers)) { - console.log(` → 마커 ${value.markers.length}개 추가`); + // // console.log(` → 마커 ${value.markers.length}개 추가`); allMarkers.push(...value.markers); } // 폴리곤 병합 if (value.polygons && Array.isArray(value.polygons)) { - console.log(` → 폴리곤 ${value.polygons.length}개 추가`); + // // console.log(` → 폴리곤 ${value.polygons.length}개 추가`); allPolygons.push(...value.polygons); } } else if (result.status === "rejected") { @@ -136,9 +136,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } }); - console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`); - console.log("📍 최종 마커 데이터:", allMarkers); - console.log("🔷 최종 폴리곤 데이터:", allPolygons); + // // console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`); + // // console.log("📍 최종 마커 데이터:", allMarkers); + // // console.log("🔷 최종 폴리곤 데이터:", allPolygons); setMarkers(allMarkers); setPolygons(allPolygons); @@ -153,13 +153,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); // REST API 데이터 로딩 const loadRestApiData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { - console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); + // // console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); if (!source.endpoint) { throw new Error("API endpoint가 없습니다."); @@ -215,10 +215,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 텍스트 형식 데이터 체크 (기상청 API 등) if (data && typeof data === 'object' && data.text && typeof data.text === 'string') { - console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); + // // console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(data.text); if (parsedData.length > 0) { - console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`); + // // console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`); // 컬럼 매핑 적용 const mappedData = applyColumnMapping(parsedData, source.columnMapping); return convertToMapData(mappedData, source.name || source.id || "API", source.mapDisplayType, source); @@ -244,7 +244,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // Database 데이터 로딩 const loadDatabaseData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { - console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); + // // console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); if (!source.query) { throw new Error("SQL 쿼리가 없습니다."); @@ -287,7 +287,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // XML 데이터 파싱 (UTIC API 등) const parseXmlData = (xmlText: string): any[] => { try { - console.log(" 📄 XML 파싱 시작"); + // // console.log(" 📄 XML 파싱 시작"); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); @@ -306,7 +306,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { results.push(obj); } - console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`); + // // console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`); return results; } catch (error) { console.error(" ❌ XML 파싱 실패:", error); @@ -317,11 +317,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 텍스트 데이터 파싱 (CSV, 기상청 형식 등) const parseTextData = (text: string): any[] => { try { - console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500)); + // // console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500)); // XML 형식 감지 if (text.trim().startsWith("")) { - console.log(" 📄 XML 형식 데이터 감지"); + // // console.log(" 📄 XML 형식 데이터 감지"); return parseXmlData(text); } @@ -333,7 +333,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { !trimmed.startsWith('---'); }); - console.log(` 📝 유효한 라인: ${lines.length}개`); + // // console.log(` 📝 유효한 라인: ${lines.length}개`); if (lines.length === 0) return []; @@ -344,7 +344,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const line = lines[i]; const values = line.split(',').map(v => v.trim().replace(/,=$/g, '')); - console.log(` 라인 ${i}:`, values); + // // console.log(` 라인 ${i}:`, values); // 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명 if (values.length >= 4) { @@ -364,11 +364,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { obj.name = obj.subRegion || obj.region || obj.code; result.push(obj); - console.log(` ✅ 파싱 성공:`, obj); + // console.log(` ✅ 파싱 성공:`, obj); } } - console.log(" 📊 최종 파싱 결과:", result.length, "개"); + // // console.log(" 📊 최종 파싱 결과:", result.length, "개"); return result; } catch (error) { console.error(" ❌ 텍스트 파싱 오류:", error); @@ -383,9 +383,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { mapDisplayType?: "auto" | "marker" | "polygon", dataSource?: ChartDataSource ): { markers: MarkerData[]; polygons: PolygonData[] } => { - console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행"); - console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`); - console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor); + // // console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행"); + // // console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`); + // // console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor); if (rows.length === 0) return { markers: [], polygons: [] }; @@ -393,13 +393,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const polygons: PolygonData[] = []; rows.forEach((row, index) => { - console.log(` 행 ${index}:`, row); + // // console.log(` 행 ${index}:`, row); // 텍스트 데이터 체크 (기상청 API 등) if (row && typeof row === 'object' && row.text && typeof row.text === 'string') { - console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); + // // console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(row.text); - console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`); + // // console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`); // 파싱된 데이터를 재귀적으로 변환 (색상 정보 전달) const result = convertToMapData(parsedData, sourceName, mapDisplayType, dataSource); @@ -410,11 +410,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 폴리곤 데이터 체크 (coordinates 필드가 배열인 경우 또는 강제 polygon 모드) if (row.coordinates && Array.isArray(row.coordinates) && row.coordinates.length > 0) { - console.log(` → coordinates 발견:`, row.coordinates.length, "개"); + // // console.log(` → coordinates 발견:`, row.coordinates.length, "개"); // coordinates가 [lat, lng] 배열의 배열인지 확인 const firstCoord = row.coordinates[0]; if (Array.isArray(firstCoord) && firstCoord.length === 2) { - console.log(` → 폴리곤으로 처리:`, row.name); + // console.log(` → 폴리곤으로 처리:`, row.name); polygons.push({ id: row.id || row.code || `polygon-${index}`, name: row.name || row.title || `영역 ${index + 1}`, @@ -431,7 +431,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 지역명으로 해상 구역 확인 (auto 또는 polygon 모드일 때만) const regionName = row.name || row.area || row.region || row.location || row.subRegion; if (regionName && MARITIME_ZONES[regionName] && mapDisplayType !== "marker") { - console.log(` → 해상 구역 발견: ${regionName}, 폴리곤으로 처리`); + // // console.log(` → 해상 구역 발견: ${regionName}, 폴리곤으로 처리`); polygons.push({ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성 name: regionName, @@ -451,24 +451,24 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 위도/경도가 없으면 지역 코드/지역명으로 변환 시도 if ((lat === undefined || lng === undefined) && (row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId)) { const regionCode = row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId; - console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`); + // // console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`); const coords = getCoordinatesByRegionCode(regionCode); if (coords) { lat = coords.lat; lng = coords.lng; - console.log(` → 변환 성공: (${lat}, ${lng})`); + // console.log(` → 변환 성공: (${lat}, ${lng})`); } } // 지역명으로도 시도 if ((lat === undefined || lng === undefined) && (row.name || row.area || row.region || row.location)) { const regionName = row.name || row.area || row.region || row.location; - console.log(` → 지역명 발견: ${regionName}, 위도/경도 변환 시도`); + // // console.log(` → 지역명 발견: ${regionName}, 위도/경도 변환 시도`); const coords = getCoordinatesByRegionName(regionName); if (coords) { lat = coords.lat; lng = coords.lng; - console.log(` → 변환 성공: (${lat}, ${lng})`); + // console.log(` → 변환 성공: (${lat}, ${lng})`); } } @@ -476,7 +476,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { if (mapDisplayType === "polygon") { const regionName = row.name || row.subRegion || row.region || row.area; if (regionName) { - console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); + // console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); polygons.push({ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, name: regionName, @@ -487,14 +487,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { color: dataSource?.polygonColor || getColorByStatus(row.status || row.level), }); } else { - console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`); + // console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`); } return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음 } // 위도/경도가 있고 marker 모드가 아니면 마커로 처리 if (lat !== undefined && lng !== undefined && mapDisplayType !== "polygon") { - console.log(` → 마커로 처리: (${lat}, ${lng})`); + // // console.log(` → 마커로 처리: (${lat}, ${lng})`); markers.push({ id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성 lat: Number(lat), @@ -511,7 +511,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 위도/경도가 없는 육지 지역 → 폴리곤으로 추가 (GeoJSON 매칭용) const regionName = row.name || row.subRegion || row.region || row.area; if (regionName) { - console.log(` 📍 위도/경도 없지만 지역명 있음: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); + // console.log(` 📍 위도/경도 없지만 지역명 있음: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); polygons.push({ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, name: regionName, @@ -522,13 +522,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { color: dataSource?.polygonColor || getColorByStatus(row.status || row.level), }); } else { - console.log(` ⚠️ 위도/경도 없고 지역명도 없음 - 스킵`); - console.log(` 데이터:`, row); + // console.log(` ⚠️ 위도/경도 없고 지역명도 없음 - 스킵`); + // console.log(` 데이터:`, row); } } }); - console.log(`✅ ${sourceName}: 마커 ${markers.length}개, 폴리곤 ${polygons.length}개 변환 완료`); + // // console.log(`✅ ${sourceName}: 마커 ${markers.length}개, 폴리곤 ${polygons.length}개 변환 완료`); return { markers, polygons }; }; @@ -757,7 +757,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { try { const response = await fetch("/geojson/korea-municipalities.json"); const data = await response.json(); - console.log("🗺️ GeoJSON 로드 완료:", data.features?.length, "개 시/군/구"); + // // console.log("🗺️ GeoJSON 로드 완료:", data.features?.length, "개 시/군/구"); setGeoJsonData(data); } catch (err) { console.error("❌ GeoJSON 로드 실패:", err); @@ -769,11 +769,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 초기 로드 useEffect(() => { const dataSources = element?.dataSources || element?.chartConfig?.dataSources; - console.log("🔄 useEffect 트리거! dataSources:", dataSources); + // // console.log("🔄 useEffect 트리거! dataSources:", dataSources); if (dataSources && dataSources.length > 0) { loadMultipleDataSources(); } else { - console.log("⚠️ dataSources가 없거나 비어있음"); + // // console.log("⚠️ dataSources가 없거나 비어있음"); setMarkers([]); setPolygons([]); } @@ -791,15 +791,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); @@ -876,11 +876,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { {/* 폴리곤 렌더링 */} {/* GeoJSON 렌더링 (육지 지역 경계선) */} {(() => { - console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, { - geoJsonData: !!geoJsonData, - polygonsLength: polygons.length, - polygonNames: polygons.map(p => p.name), - }); + // console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, { + // geoJsonData: !!geoJsonData, + // polygonsLength: polygons.length, + // polygonNames: polygons.map(p => p.name), + // }); return null; })()} {geoJsonData && polygons.length > 0 ? ( @@ -897,31 +897,31 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 정확한 매칭 if (p.name === sigName) { - console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`); + // console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`); return true; } if (p.name === ctpName) { - console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`); + // console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`); return true; } // 부분 매칭 (GeoJSON 지역명에 폴리곤 이름이 포함되는지) if (sigName && sigName.includes(p.name)) { - console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`); + // console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`); return true; } if (ctpName && ctpName.includes(p.name)) { - console.log(`✅ 부분 매칭: ${ctpName} includes ${p.name}`); + // console.log(`✅ 부분 매칭: ${ctpName} includes ${p.name}`); return true; } // 역방향 매칭 (폴리곤 이름에 GeoJSON 지역명이 포함되는지) if (sigName && p.name.includes(sigName)) { - console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`); + // console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`); return true; } if (ctpName && p.name.includes(ctpName)) { - console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`); + // console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`); return true; } @@ -969,7 +969,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { }} /> ) : ( - <>{console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`)} + <> + {/* console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`) */} + )} {/* 폴리곤 렌더링 (해상 구역만) */} diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 6ac1765a..fd8b60fd 100644 --- a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx @@ -39,12 +39,12 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp const parseTextData = (text: string): any[] => { // XML 형식 감지 if (text.trim().startsWith("")) { - console.log("📄 XML 형식 데이터 감지"); + // console.log("📄 XML 형식 데이터 감지"); return parseXmlData(text); } // CSV 형식 (기상청 특보) - console.log("📄 CSV 형식 데이터 감지"); + // console.log("📄 CSV 형식 데이터 감지"); const lines = text.split("\n").filter((line) => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith("#") && trimmed !== "="; @@ -98,7 +98,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp results.push(obj); } - console.log(`✅ XML 파싱 완료: ${results.length}개 레코드`); + // console.log(`✅ XML 파싱 완료: ${results.length}개 레코드`); return results; } catch (error) { console.error("❌ XML 파싱 실패:", error); @@ -107,14 +107,78 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp }; const loadRestApiData = useCallback(async (source: ChartDataSource) => { - if (!source.endpoint) { + // 🆕 외부 연결 ID가 있으면 먼저 외부 연결 정보를 가져옴 + let actualEndpoint = source.endpoint; + let actualQueryParams = source.queryParams; + let actualHeaders = source.headers; + + if (source.externalConnectionId) { + // console.log("🔗 외부 연결 ID 감지:", source.externalConnectionId); + try { + const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); + const connection = await ExternalDbConnectionAPI.getApiConnectionById(Number(source.externalConnectionId)); + + if (connection) { + // console.log("✅ 외부 연결 정보 가져옴:", connection); + + // 전체 엔드포인트 URL 생성 + actualEndpoint = connection.endpoint_path + ? `${connection.base_url}${connection.endpoint_path}` + : connection.base_url; + + // console.log("📍 실제 엔드포인트:", actualEndpoint); + + // 기본 헤더 적용 + const headers: any[] = []; + if (connection.default_headers && Object.keys(connection.default_headers).length > 0) { + Object.entries(connection.default_headers).forEach(([key, value]) => { + headers.push({ key, value }); + }); + } + + // 인증 정보 적용 + const queryParams: any[] = []; + if (connection.auth_type && connection.auth_type !== "none" && connection.auth_config) { + const authConfig = connection.auth_config; + + if (connection.auth_type === "api-key") { + if (authConfig.keyLocation === "header" && authConfig.keyName && authConfig.keyValue) { + headers.push({ key: authConfig.keyName, value: authConfig.keyValue }); + // console.log("🔑 API Key 헤더 추가:", authConfig.keyName); + } else if (authConfig.keyLocation === "query" && authConfig.keyName && authConfig.keyValue) { + const actualKeyName = authConfig.keyName === "apiKey" ? "key" : authConfig.keyName; + queryParams.push({ key: actualKeyName, value: authConfig.keyValue }); + // console.log("🔑 API Key 쿼리 파라미터 추가:", actualKeyName); + } + } else if (connection.auth_type === "bearer" && authConfig.token) { + headers.push({ key: "Authorization", value: `Bearer ${authConfig.token}` }); + // console.log("🔑 Bearer Token 헤더 추가"); + } else if (connection.auth_type === "basic" && authConfig.username && authConfig.password) { + const credentials = btoa(`${authConfig.username}:${authConfig.password}`); + headers.push({ key: "Authorization", value: `Basic ${credentials}` }); + // console.log("🔑 Basic Auth 헤더 추가"); + } + } + + actualHeaders = headers; + actualQueryParams = queryParams; + + // console.log("✅ 최종 헤더:", actualHeaders); + // console.log("✅ 최종 쿼리 파라미터:", actualQueryParams); + } + } catch (err) { + console.error("❌ 외부 연결 정보 가져오기 실패:", err); + } + } + + if (!actualEndpoint) { throw new Error("API endpoint가 없습니다."); } // 쿼리 파라미터 처리 const queryParamsObj: Record = {}; - if (source.queryParams && Array.isArray(source.queryParams)) { - source.queryParams.forEach((param) => { + if (actualQueryParams && Array.isArray(actualQueryParams)) { + actualQueryParams.forEach((param) => { if (param.key && param.value) { queryParamsObj[param.key] = param.value; } @@ -123,34 +187,33 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp // 헤더 처리 const headersObj: Record = {}; - if (source.headers && Array.isArray(source.headers)) { - source.headers.forEach((header) => { + if (actualHeaders && Array.isArray(actualHeaders)) { + actualHeaders.forEach((header) => { if (header.key && header.value) { headersObj[header.key] = header.value; } }); } - console.log("🌐 API 호출 준비:", { - endpoint: source.endpoint, - queryParams: queryParamsObj, - headers: headersObj, - }); - console.log("🔍 원본 source.queryParams:", source.queryParams); - console.log("🔍 원본 source.headers:", source.headers); + // console.log("🌐 API 호출 준비:", { + // endpoint: actualEndpoint, + // queryParams: queryParamsObj, + // headers: headersObj, + // }); const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ - url: source.endpoint, + url: actualEndpoint, method: "GET", headers: headersObj, queryParams: queryParamsObj, }), }); - console.log("🌐 API 응답 상태:", response.status); + // console.log("🌐 API 응답 상태:", response.status); if (!response.ok) { throw new Error(`HTTP ${response.status}`); @@ -163,22 +226,22 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp let apiData = result.data; - console.log("🔍 API 응답 데이터 타입:", typeof apiData); - console.log("🔍 API 응답 데이터 (처음 500자):", typeof apiData === "string" ? apiData.substring(0, 500) : JSON.stringify(apiData).substring(0, 500)); + // console.log("🔍 API 응답 데이터 타입:", typeof apiData); + // console.log("🔍 API 응답 데이터 (처음 500자):", typeof apiData === "string" ? apiData.substring(0, 500) : JSON.stringify(apiData).substring(0, 500)); // 백엔드가 {text: "XML..."} 형태로 감싼 경우 처리 if (apiData && typeof apiData === "object" && apiData.text && typeof apiData.text === "string") { - console.log("📦 백엔드가 text 필드로 감싼 데이터 감지"); + // console.log("📦 백엔드가 text 필드로 감싼 데이터 감지"); apiData = parseTextData(apiData.text); - console.log("✅ 파싱 성공:", apiData.length, "개 행"); + // console.log("✅ 파싱 성공:", apiData.length, "개 행"); } else if (typeof apiData === "string") { - console.log("📄 텍스트 형식 데이터 감지, 파싱 시도"); + // console.log("📄 텍스트 형식 데이터 감지, 파싱 시도"); apiData = parseTextData(apiData); - console.log("✅ 파싱 성공:", apiData.length, "개 행"); + // console.log("✅ 파싱 성공:", apiData.length, "개 행"); } else if (Array.isArray(apiData)) { - console.log("✅ 이미 배열 형태의 데이터입니다."); + // console.log("✅ 이미 배열 형태의 데이터입니다."); } else { - console.log("⚠️ 예상치 못한 데이터 형식입니다. 배열로 변환 시도."); + // console.log("⚠️ 예상치 못한 데이터 형식입니다. 배열로 변환 시도."); apiData = [apiData]; } @@ -220,7 +283,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp }, []); const convertToAlerts = useCallback((rows: any[], sourceName: string): Alert[] => { - console.log("🔄 convertToAlerts 호출:", rows.length, "개 행"); + // console.log("🔄 convertToAlerts 호출:", rows.length, "개 행"); return rows.map((row: any, index: number) => { // 타입 결정 (UTIC XML 기준) @@ -325,7 +388,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp source: sourceName, }; - console.log(` ✅ Alert ${index}:`, alert); + // console.log(` ✅ Alert ${index}:`, alert); return alert; }); }, []); @@ -338,19 +401,19 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp setLoading(true); setError(null); - console.log("🔄 RiskAlertTestWidget 데이터 로딩 시작:", dataSources.length, "개 소스"); + // console.log("🔄 RiskAlertTestWidget 데이터 로딩 시작:", dataSources.length, "개 소스"); try { const results = await Promise.allSettled( dataSources.map(async (source, index) => { - console.log(`📡 데이터 소스 ${index + 1} 로딩 중:`, source.name, source.type); + // console.log(`📡 데이터 소스 ${index + 1} 로딩 중:`, source.name, source.type); if (source.type === "api") { const alerts = await loadRestApiData(source); - console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); + // console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); return alerts; } else { const alerts = await loadDatabaseData(source); - console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); + // console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); return alerts; } }) @@ -359,14 +422,14 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp const allAlerts: Alert[] = []; results.forEach((result, index) => { if (result.status === "fulfilled") { - console.log(`✅ 결과 ${index + 1} 병합:`, result.value.length, "개 알림"); + // console.log(`✅ 결과 ${index + 1} 병합:`, result.value.length, "개 알림"); allAlerts.push(...result.value); } else { console.error(`❌ 결과 ${index + 1} 실패:`, result.reason); } }); - console.log("✅ 총", allAlerts.length, "개 알림 로딩 완료"); + // console.log("✅ 총", allAlerts.length, "개 알림 로딩 완료"); allAlerts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); setAlerts(allAlerts); setLastRefreshTime(new Date()); @@ -380,7 +443,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); @@ -403,15 +466,15 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); @@ -516,7 +579,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
{/* 컨텐츠 */} -
+