diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 090985ba..6d254cfe 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -916,7 +916,7 @@ export function CanvasElement({ ) : element.type === "widget" && element.subtype === "weather" ? ( // 날씨 위젯 렌더링
- +
) : element.type === "widget" && element.subtype === "exchange" ? ( // 환율 위젯 렌더링 diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx index 03abee6f..aad0e2b5 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx @@ -2155,23 +2155,23 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi - {materials.map((material, index) => { - const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER"; - const keyColumn = hierarchyConfig?.material?.keyColumn || "STKKEY"; - const displayColumns = hierarchyConfig?.material?.displayColumns || []; + {materials.map((material, index) => { + const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER"; + const keyColumn = hierarchyConfig?.material?.keyColumn || "STKKEY"; + const displayColumns = hierarchyConfig?.material?.displayColumns || []; const layerNumber = material[layerColumn] || index + 1; - return ( + return ( {layerNumber}단 {displayColumns.map((col) => ( {material[col.column] || "-"} - ))} + ))} - ); - })} + ); + })} diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx index a702a047..6d1f4a31 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx @@ -660,25 +660,25 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) - {materials.map((material, index) => { + {materials.map((material, index) => { const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER"; - const displayColumns = hierarchyConfig?.material?.displayColumns || []; - return ( + const displayColumns = hierarchyConfig?.material?.displayColumns || []; + return ( {material[layerColumn]}단 - {displayColumns.map((colConfig: any) => ( + {displayColumns.map((colConfig: any) => ( {material[colConfig.column] || "-"} - ))} + ))} - ); - })} + ); + })} diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 30ee99a5..1806ff34 100644 --- a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -8,6 +8,9 @@ import { RefreshCw, AlertTriangle, Cloud, Construction, Database as DatabaseIcon import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types"; import { getApiUrl } from "@/lib/utils/apiUrl"; +// 컴팩트 모드 임계값 (픽셀) +const COMPACT_HEIGHT_THRESHOLD = 180; + type AlertType = "accident" | "weather" | "construction" | "system" | "security" | "other"; interface Alert { @@ -31,6 +34,29 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp const [error, setError] = useState(null); const [filter, setFilter] = useState("all"); const [lastRefreshTime, setLastRefreshTime] = useState(null); + + // 컨테이너 높이 측정을 위한 ref + const containerRef = useRef(null); + const [containerHeight, setContainerHeight] = useState(300); + + // 컴팩트 모드 여부 (element.size.height 또는 실제 컨테이너 높이 기반) + const isCompact = element?.size?.height + ? element.size.height < COMPACT_HEIGHT_THRESHOLD + : containerHeight < COMPACT_HEIGHT_THRESHOLD; + + // 컨테이너 높이 측정 + useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerHeight(entry.contentRect.height); + } + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; @@ -549,8 +575,57 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp ); } + // 통계 계산 + const stats = { + accident: alerts.filter((a) => a.type === "accident").length, + weather: alerts.filter((a) => a.type === "weather").length, + construction: alerts.filter((a) => a.type === "construction").length, + high: alerts.filter((a) => a.severity === "high").length, + }; + + // 컴팩트 모드 렌더링 - 알림 목록만 스크롤 + if (isCompact) { + return ( +
+ {filteredAlerts.length === 0 ? ( +
+

알림 없음

+
+ ) : ( + filteredAlerts.map((alert, idx) => ( +
+
+ {getTypeIcon(alert.type)} + {alert.title} + + {alert.severity === "high" ? "긴급" : alert.severity === "medium" ? "주의" : "정보"} + +
+ {alert.location && ( +

{alert.location}

+ )} +
+ )) + )} +
+ ); + } + + // 일반 모드 렌더링 return ( -
+
{/* 헤더 */}
@@ -631,7 +706,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
{alert.location && ( -

📍 {alert.location}

+

{alert.location}

)}

{alert.description}

diff --git a/frontend/components/dashboard/widgets/RiskAlertWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertWidget.tsx index 3e638f3c..7728cc5d 100644 --- a/frontend/components/dashboard/widgets/RiskAlertWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertWidget.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -8,6 +8,9 @@ import { RefreshCw, AlertTriangle, Cloud, Construction } from "lucide-react"; import { apiClient } from "@/lib/api/client"; import { DashboardElement } from "@/components/admin/dashboard/types"; +// 컴팩트 모드 임계값 (픽셀) +const COMPACT_HEIGHT_THRESHOLD = 180; + // 알림 타입 type AlertType = "accident" | "weather" | "construction"; @@ -32,6 +35,29 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) { const [filter, setFilter] = useState("all"); const [lastUpdated, setLastUpdated] = useState(null); const [newAlertIds, setNewAlertIds] = useState>(new Set()); + + // 컨테이너 높이 측정을 위한 ref + const containerRef = useRef(null); + const [containerHeight, setContainerHeight] = useState(300); + + // 컴팩트 모드 여부 (element.size.height 또는 실제 컨테이너 높이 기반) + const isCompact = element?.size?.height + ? element.size.height < COMPACT_HEIGHT_THRESHOLD + : containerHeight < COMPACT_HEIGHT_THRESHOLD; + + // 컨테이너 높이 측정 + useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerHeight(entry.contentRect.height); + } + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); // 데이터 로드 (백엔드 캐시 조회) const loadData = async () => { @@ -176,8 +202,49 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) { high: alerts.filter((a) => a.severity === "high").length, }; + // 컴팩트 모드 렌더링 - 알림 목록만 스크롤 + if (isCompact) { + return ( +
+ {filteredAlerts.length === 0 ? ( +
+

알림 없음

+
+ ) : ( + filteredAlerts.map((alert) => ( +
+
+ {getAlertIcon(alert.type)} + {alert.title} + + {alert.severity === "high" ? "긴급" : alert.severity === "medium" ? "주의" : "정보"} + +
+ {alert.location && ( +

{alert.location}

+ )} +
+ )) + )} +
+ ); + } + + // 일반 모드 렌더링 return ( -
+
{/* 헤더 */}
@@ -294,7 +361,7 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) { {/* 안내 메시지 */}
- 💡 1분마다 자동으로 업데이트됩니다 + 1분마다 자동으로 업데이트됩니다
); diff --git a/frontend/components/dashboard/widgets/WeatherWidget.tsx b/frontend/components/dashboard/widgets/WeatherWidget.tsx index 56c3aaf6..3d1d7157 100644 --- a/frontend/components/dashboard/widgets/WeatherWidget.tsx +++ b/frontend/components/dashboard/widgets/WeatherWidget.tsx @@ -3,9 +3,10 @@ /** * 날씨 위젯 컴포넌트 * - 실시간 날씨 정보를 표시 + * - 컴팩트 모드: 높이가 작을 때 핵심 정보만 표시 */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { getWeather, WeatherData } from '@/lib/api/openApi'; import { Cloud, @@ -26,6 +27,9 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command import { cn } from '@/lib/utils'; import { DashboardElement } from '@/components/admin/dashboard/types'; +// 컴팩트 모드 임계값 (픽셀) +const COMPACT_HEIGHT_THRESHOLD = 180; + interface WeatherWidgetProps { element?: DashboardElement; city?: string; @@ -45,6 +49,29 @@ export default function WeatherWidget({ const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); + // 컨테이너 높이 측정을 위한 ref + const containerRef = useRef(null); + const [containerHeight, setContainerHeight] = useState(300); + + // 컴팩트 모드 여부 (element.size.height 또는 실제 컨테이너 높이 기반) + const isCompact = element?.size?.height + ? element.size.height < COMPACT_HEIGHT_THRESHOLD + : containerHeight < COMPACT_HEIGHT_THRESHOLD; + + // 컨테이너 높이 측정 + useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerHeight(entry.contentRect.height); + } + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); + // 표시할 날씨 정보 선택 const [selectedItems, setSelectedItems] = useState([ 'temperature', @@ -323,12 +350,105 @@ export default function WeatherWidget({ ); } + // 날씨 아이콘 렌더링 헬퍼 + const renderWeatherIcon = (weatherMain: string, size: "sm" | "md" = "sm") => { + const iconClass = size === "sm" ? "h-5 w-5" : "h-8 w-8"; + switch (weatherMain.toLowerCase()) { + case 'clear': + return ; + case 'clouds': + return ; + case 'rain': + case 'drizzle': + return ; + case 'snow': + return ; + default: + return ; + } + }; + + // 컴팩트 모드 렌더링 + if (isCompact) { + return ( +
+ {/* 컴팩트 헤더 - 도시명, 온도, 날씨 아이콘 한 줄에 표시 */} +
+
+ {renderWeatherIcon(weather.weatherMain, "md")} +
+
+ + {weather.temperature}°C + + + {weather.weatherDescription} + +
+ + + + + + + + + 도시를 찾을 수 없습니다. + + {cities.map((city) => ( + { + handleCityChange(currentValue === selectedCity ? selectedCity : currentValue); + setOpen(false); + }} + > + + {city.label} + + ))} + + + + + +
+
+ +
+
+ ); + } + + // 일반 모드 렌더링 return ( -
+
{/* 헤더 */}
-

🌤️ {element?.customTitle || "날씨"}

+

{element?.customTitle || "날씨"}

@@ -438,22 +558,7 @@ export default function WeatherWidget({
- {(() => { - const iconClass = "h-5 w-5"; - switch (weather.weatherMain.toLowerCase()) { - case 'clear': - return ; - case 'clouds': - return ; - case 'rain': - case 'drizzle': - return ; - case 'snow': - return ; - default: - return ; - } - })()} + {renderWeatherIcon(weather.weatherMain)}
diff --git a/frontend/components/report/designer/QueryManager.tsx b/frontend/components/report/designer/QueryManager.tsx index e9ccb813..39b0d173 100644 --- a/frontend/components/report/designer/QueryManager.tsx +++ b/frontend/components/report/designer/QueryManager.tsx @@ -274,15 +274,15 @@ export function QueryManager() {
- -
+ > + + +
{/* 쿼리 이름 */}
diff --git a/frontend/components/report/designer/ReportPreviewModal.tsx b/frontend/components/report/designer/ReportPreviewModal.tsx index 2ff70c73..ded27f37 100644 --- a/frontend/components/report/designer/ReportPreviewModal.tsx +++ b/frontend/components/report/designer/ReportPreviewModal.tsx @@ -486,11 +486,11 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) } } return component; - }), - ); + }), + ); return { ...page, components: componentsWithBase64 }; - }), - ); + }), + ); // 쿼리 결과 수집 const queryResults: Record[] }> = {};