-
-
- {element?.customTitle || "리스트"}
-
-
- {dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행
- {lastRefreshTime && (
-
- • {lastRefreshTime.toLocaleTimeString("ko-KR")}
-
+
+ {/* 컴팩트 모드 (세로 1칸) - 캐러셀 형태로 한 건씩 표시 */}
+ {isCompactHeight ? (
+
+ {data && data.rows.length > 0 && displayColumns.length > 0 ? (
+
+ {/* 이전 버튼 */}
+
setCurrentPage((p) => Math.max(1, p - 1))}
+ disabled={currentPage === 1}
+ >
+
+
+
+ {/* 현재 데이터 */}
+
+ {displayColumns.slice(0, 4).map((field, fieldIdx) => (
+
+ {String(data.rows[currentPage - 1]?.[field] ?? "").substring(0, 25)}
+ {fieldIdx < Math.min(displayColumns.length, 4) - 1 && " | "}
+
+ ))}
+
+
+ {/* 다음 버튼 */}
+
setCurrentPage((p) => Math.min(data.rows.length, p + 1))}
+ disabled={currentPage === data.rows.length}
+ >
+
+
+
+ ) : (
+
데이터 없음
+ )}
+
+ {/* 현재 위치 표시 (작게) */}
+ {data && data.rows.length > 0 && (
+
+ {currentPage} / {data.rows.length}
+
+ )}
+
+ ) : (
+ <>
+ {/* 헤더 */}
+
+
+
+ {element?.customTitle || "리스트"}
+
+
+ {dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행
+ {lastRefreshTime && (
+
+ • {lastRefreshTime.toLocaleTimeString("ko-KR")}
+
+ )}
+
+
+
+
+
+ 새로고침
+
+ {isLoading && }
+
+
+
+ {/* 컨텐츠 */}
+
+ {error ? (
+
+ ) : !dataSources || dataSources.length === 0 ? (
+
+ ) : !data || data.rows.length === 0 ? (
+
+ ) : config.viewMode === "card" ? (
+ renderCards()
+ ) : (
+ renderTable()
)}
-
-
-
-
-
- 새로고침
-
- {isLoading && }
-
-
+
- {/* 컨텐츠 */}
-
- {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})
-
-
- setCurrentPage((p) => Math.max(1, p - 1))}
- disabled={currentPage === 1}
- >
- 이전
-
- setCurrentPage((p) => Math.min(totalPages, p + 1))}
- disabled={currentPage === totalPages}
- >
- 다음
-
-
-
+ {/* 페이지네이션 */}
+ {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && (
+
+
+ 총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages})
+
+
+ setCurrentPage((p) => Math.max(1, p - 1))}
+ disabled={currentPage === 1}
+ >
+ 이전
+
+ setCurrentPage((p) => Math.min(totalPages, p + 1))}
+ disabled={currentPage === totalPages}
+ >
+ 다음
+
+
+
+ )}
+ >
)}
{/* 행 상세 팝업 */}
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.find((city) => city.value === selectedCity)?.label || '도시 선택'}
+
+
+
+
+
+
+
+ 도시를 찾을 수 없습니다.
+
+ {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() {
-
handleDeleteQuery(query.id, e)}
+ handleDeleteQuery(query.id, e)}
className="h-7 w-7 shrink-0 p-0"
- >
-
-
-
+ >
+
+
+
{/* 쿼리 이름 */}
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[] }> = {};