diff --git a/frontend/components/admin/dashboard/widgets/ListWidget.tsx b/frontend/components/admin/dashboard/widgets/ListWidget.tsx index 2f05a927..194f7210 100644 --- a/frontend/components/admin/dashboard/widgets/ListWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/ListWidget.tsx @@ -14,7 +14,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { getApiUrl } from "@/lib/utils/apiUrl"; -import { Truck, Clock, MapPin, Package, Info, ChevronLeft, ChevronRight } from "lucide-react"; +import { Truck, Clock, MapPin, Package, Info } from "lucide-react"; interface ListWidgetProps { element: DashboardElement; @@ -32,8 +32,6 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); - const [containerHeight, setContainerHeight] = useState(0); - const containerRef = React.useRef(null); // 행 상세 팝업 상태 const [detailPopupOpen, setDetailPopupOpen] = useState(false); @@ -41,25 +39,6 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const [detailPopupLoading, setDetailPopupLoading] = useState(false); const [additionalDetailData, setAdditionalDetailData] = useState | null>(null); - // 컨테이너 높이 감지 - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - setContainerHeight(entry.contentRect.height); - } - }); - - resizeObserver.observe(container); - return () => resizeObserver.disconnect(); - }, []); - - // 컴팩트 모드 여부 (높이 300px 이하 또는 element 높이가 300px 이하) - const elementHeight = element?.size?.height || 0; - const isCompactHeight = elementHeight > 0 ? elementHeight < 300 : (containerHeight > 0 && containerHeight < 300); - const config = element.listConfig || { columnMode: "auto", viewMode: "table", @@ -562,64 +541,14 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const paginatedRows = config.enablePagination ? data.rows.slice(startIdx, endIdx) : data.rows; return ( -
- {/* 컴팩트 모드 (세로 1칸) - 캐러셀 형태로 한 건씩 표시 */} - {isCompactHeight ? ( -
- {data && data.rows.length > 0 && displayColumns.filter((col) => col.visible).length > 0 ? ( -
- {/* 이전 버튼 */} - +
+ {/* 제목 - 항상 표시 */} +
+

{element.customTitle || element.title}

+
- {/* 현재 데이터 */} -
- {displayColumns.filter((col) => col.visible).slice(0, 4).map((col, colIdx) => ( - - {String(data.rows[currentPage - 1]?.[col.dataKey || col.field] ?? "").substring(0, 25)} - {colIdx < Math.min(displayColumns.filter((c) => c.visible).length, 4) - 1 && " | "} - - ))} -
- - {/* 다음 버튼 */} - -
- ) : ( -
데이터 없음
- )} - - {/* 현재 위치 표시 (작게) */} - {data && data.rows.length > 0 && ( -
- {currentPage} / {data.rows.length} -
- )} -
- ) : ( - <> - {/* 제목 - 항상 표시 */} -
-

{element.customTitle || element.title}

-
- - {/* 테이블 뷰 */} - {config.viewMode === "table" && ( + {/* 테이블 뷰 */} + {config.viewMode === "table" && (
{config.showHeader && ( @@ -713,38 +642,36 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { )} - {/* 페이지네이션 */} - {config.enablePagination && totalPages > 1 && ( -
-
- {startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개 -
-
- -
- {currentPage} - / - {totalPages} -
- -
+ {/* 페이지네이션 */} + {config.enablePagination && totalPages > 1 && ( +
+
+ {startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개 +
+
+ +
+ {currentPage} + / + {totalPages}
- )} - + +
+
)} {/* 행 상세 팝업 */} diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index a1609b1a..d1303d10 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -13,7 +13,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Loader2, RefreshCw, Truck, Clock, MapPin, Package, Info, ChevronLeft, ChevronRight } from "lucide-react"; +import { Loader2, RefreshCw, Truck, Clock, MapPin, Package, Info } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; import { getApiUrl } from "@/lib/utils/apiUrl"; @@ -41,8 +41,6 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [lastRefreshTime, setLastRefreshTime] = useState(null); - const [containerHeight, setContainerHeight] = useState(0); - const containerRef = React.useRef(null); // 행 상세 팝업 상태 const [detailPopupOpen, setDetailPopupOpen] = useState(false); @@ -50,25 +48,6 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const [detailPopupLoading, setDetailPopupLoading] = useState(false); const [additionalDetailData, setAdditionalDetailData] = useState | null>(null); - // 컨테이너 높이 감지 - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - setContainerHeight(entry.contentRect.height); - } - }); - - resizeObserver.observe(container); - return () => resizeObserver.disconnect(); - }, []); - - // 컴팩트 모드 여부 (높이 300px 이하 또는 element 높이가 300px 이하) - const elementHeight = element?.size?.height || 0; - const isCompactHeight = elementHeight > 0 ? elementHeight < 300 : (containerHeight > 0 && containerHeight < 300); - // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { @@ -764,139 +743,87 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { }; return ( -
- {/* 컴팩트 모드 (세로 1칸) - 캐러셀 형태로 한 건씩 표시 */} - {isCompactHeight ? ( -
- {data && data.rows.length > 0 && displayColumns.length > 0 ? ( -
- {/* 이전 버튼 */} - - - {/* 현재 데이터 */} -
- {displayColumns.slice(0, 4).map((field, fieldIdx) => ( - - {String(data.rows[currentPage - 1]?.[field] ?? "").substring(0, 25)} - {fieldIdx < Math.min(displayColumns.length, 4) - 1 && " | "} - - ))} -
- - {/* 다음 버튼 */} - -
- ) : ( -
데이터 없음
- )} - - {/* 현재 위치 표시 (작게) */} - {data && data.rows.length > 0 && ( -
- {currentPage} / {data.rows.length} -
- )} -
- ) : ( - <> - {/* 헤더 */} -
-
-

- {element?.customTitle || "리스트"} -

-

- {dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행 - {lastRefreshTime && ( - - • {lastRefreshTime.toLocaleTimeString("ko-KR")} - - )} -

-
-
- - {isLoading && } -
-
- - {/* 컨텐츠 */} -
- {error ? ( -
-

{error}

-
- ) : !dataSources || dataSources.length === 0 ? ( -
-

- 데이터 소스를 연결해주세요 -

-
- ) : !data || data.rows.length === 0 ? ( -
-

- 데이터가 없습니다 -

-
- ) : config.viewMode === "card" ? ( - renderCards() - ) : ( - renderTable() +
+ {/* 헤더 */} +
+
+

+ {element?.customTitle || "리스트"} +

+

+ {dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행 + {lastRefreshTime && ( + + • {lastRefreshTime.toLocaleTimeString("ko-KR")} + )} -

+

+
+
+ + {isLoading && } +
+
- {/* 페이지네이션 */} - {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && ( -
-
- 총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages}) -
-
- - -
-
- )} - + {/* 컨텐츠 */} +
+ {error ? ( +
+

{error}

+
+ ) : !dataSources || dataSources.length === 0 ? ( +
+

+ 데이터 소스를 연결해주세요 +

+
+ ) : !data || data.rows.length === 0 ? ( +
+

+ 데이터가 없습니다 +

+
+ ) : config.viewMode === "card" ? ( + renderCards() + ) : ( + renderTable() + )} +
+ + {/* 페이지네이션 */} + {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && ( +
+
+ 총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages}) +
+
+ + +
+
)} {/* 행 상세 팝업 */} diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 1806ff34..30ee99a5 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, useRef } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -8,9 +8,6 @@ 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 { @@ -34,29 +31,6 @@ 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; @@ -575,57 +549,8 @@ 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 ( -
+
{/* 헤더 */}
@@ -706,7 +631,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 7728cc5d..3e638f3c 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, useRef } from "react"; +import React, { useState, useEffect } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -8,9 +8,6 @@ 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"; @@ -35,29 +32,6 @@ 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 () => { @@ -202,49 +176,8 @@ 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 ( -
+
{/* 헤더 */}
@@ -361,7 +294,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 3d1d7157..56c3aaf6 100644 --- a/frontend/components/dashboard/widgets/WeatherWidget.tsx +++ b/frontend/components/dashboard/widgets/WeatherWidget.tsx @@ -3,10 +3,9 @@ /** * 날씨 위젯 컴포넌트 * - 실시간 날씨 정보를 표시 - * - 컴팩트 모드: 높이가 작을 때 핵심 정보만 표시 */ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import { getWeather, WeatherData } from '@/lib/api/openApi'; import { Cloud, @@ -27,9 +26,6 @@ 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; @@ -49,29 +45,6 @@ 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', @@ -350,105 +323,12 @@ 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 || "날씨"}

@@ -558,7 +438,22 @@ export default function WeatherWidget({
- {renderWeatherIcon(weather.weatherMain)} + {(() => { + 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 ; + } + })()}