From a3503c0b9fb9b2aa12f473e0f36c67b3b83ff1e7 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 10:26:09 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EB=B3=84=20polling=20=EC=84=A4=EC=A0=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/WidgetConfigSidebar.tsx | 78 +++++++++++++++---- .../dashboard/data-sources/MultiApiConfig.tsx | 24 ------ .../data-sources/MultiDatabaseConfig.tsx | 32 -------- frontend/components/admin/dashboard/types.ts | 4 +- .../dashboard/widgets/MapTestWidgetV2.tsx | 7 +- 5 files changed, 68 insertions(+), 77 deletions(-) diff --git a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx index 7ca9684b..d126d8d9 100644 --- a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx @@ -17,6 +17,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DatabaseConfig } from "./data-sources/DatabaseConfig"; import { ApiConfig } from "./data-sources/ApiConfig"; import { QueryEditor } from "./QueryEditor"; @@ -146,6 +147,9 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge // 커스텀 메트릭 설정 const [customMetricConfig, setCustomMetricConfig] = useState({}); + // 자동 새로고침 간격 (지도 위젯용) + const [refreshInterval, setRefreshInterval] = useState(5); + // 사이드바 열릴 때 초기화 useEffect(() => { if (isOpen && element) { @@ -155,6 +159,8 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 가져옴 setDataSources(element.dataSources || element.chartConfig?.dataSources || []); setQueryResult(null); + // 자동 새로고침 간격 초기화 + setRefreshInterval(element.chartConfig?.refreshInterval ?? 5); // 리스트 위젯 설정 초기화 if (element.subtype === "list-v2" && element.listConfig) { @@ -290,6 +296,24 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge element.subtype === "custom-metric-v2" || element.subtype === "risk-alert-v2"; + // chartConfig 구성 (위젯 타입별로 다르게 처리) + let finalChartConfig = { ...chartConfig }; + + if (isMultiDataSourceWidget) { + finalChartConfig = { + ...finalChartConfig, + dataSources: dataSources, + }; + } + + // 지도 위젯인 경우 refreshInterval 추가 + if (element.subtype === "map-summary-v2") { + finalChartConfig = { + ...finalChartConfig, + refreshInterval, + }; + } + const updatedElement: DashboardElement = { ...element, customTitle: customTitle.trim() || undefined, @@ -302,8 +326,6 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge ...(isMultiDataSourceWidget ? { dataSources: dataSources, - // chartConfig에도 dataSources 포함 (일부 위젯은 chartConfig에서 읽음) - chartConfig: { ...chartConfig, dataSources: dataSources }, } : {}), } @@ -314,21 +336,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge listConfig, } : {}), - // 차트 설정 (차트 타입이거나 차트 기능이 있는 위젯) - ...(element.type === "chart" || - element.subtype === "chart" || - ["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"].includes(element.subtype) + // 차트 설정 (모든 위젯 공통) + ...(needsDataSource(element.subtype) ? { - // 다중 데이터 소스 위젯은 chartConfig에 dataSources 포함 (빈 배열도 허용 - 연결 해제) - chartConfig: isMultiDataSourceWidget - ? { ...chartConfig, dataSources: dataSources } - : chartConfig, - // 프론트엔드 호환성을 위해 dataSources도 element에 직접 포함 (빈 배열도 허용 - 연결 해제) - ...(isMultiDataSourceWidget - ? { - dataSources: dataSources, - } - : {}), + chartConfig: finalChartConfig, } : {}), // 커스텀 메트릭 설정 @@ -356,6 +367,7 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge listConfig, chartConfig, customMetricConfig, + refreshInterval, onApply, onClose, ]); @@ -432,6 +444,40 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge + + {/* 자동 새로고침 설정 (지도 위젯 전용) */} + {element.subtype === "map-summary-v2" && ( +
+ + +

+ 위젯의 모든 데이터를 자동으로 갱신하는 주기를 설정합니다 +

+
+ )} diff --git a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx index c72cb18e..2aba31f8 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx @@ -530,30 +530,6 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M ))} - {/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */} -
- - -

- 마커 데이터를 자동으로 갱신하는 주기를 설정합니다 -

-
{/* 마커 종류 선택 (MapTestWidgetV2 전용) */}
diff --git a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx index 73b2ab4b..bdf6b6b0 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx @@ -321,38 +321,6 @@ ORDER BY 하위부서수 DESC`, />
- {/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */} -
- - -

마커 데이터를 자동으로 갱신하는 주기를 설정합니다

-
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index f5490dbf..e0fdb3a1 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -155,7 +155,6 @@ export interface ChartDataSource { jsonPath?: string; // JSON 응답에서 데이터 추출 경로 (예: "data.results") // 공통 - refreshInterval?: number; // 자동 새로고침 (초, 0이면 수동) lastExecuted?: string; // 마지막 실행 시간 lastError?: string; // 마지막 오류 메시지 mapDisplayType?: "auto" | "marker" | "polygon"; // 지도 표시 방식 (auto: 자동, marker: 마커, polygon: 영역) @@ -184,6 +183,9 @@ export interface ChartConfig { // 다중 데이터 소스 (테스트 위젯용) dataSources?: ChartDataSource[]; // 여러 데이터 소스 (REST API + Database 혼합 가능) + // 위젯 레벨 설정 (MapTestWidgetV2용) + refreshInterval?: number; // 위젯 전체 자동 새로고침 간격 (초, 0이면 수동) + // 멀티 차트 설정 (ChartTestWidget용) chartType?: string; // 차트 타입 (line, bar, pie, etc.) mergeMode?: boolean; // 데이터 병합 모드 (여러 데이터 소스를 하나의 라인/바로 합침) diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index dafc40fa..5df1663a 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -916,9 +916,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 즉시 첫 로드 (마커 데이터) loadMultipleDataSources(); - // 첫 번째 데이터 소스의 새로고침 간격 사용 (초) - const firstDataSource = dataSources[0]; - const refreshInterval = firstDataSource?.refreshInterval ?? 5; + // 위젯 레벨의 새로고침 간격 사용 (초) + const refreshInterval = element?.chartConfig?.refreshInterval ?? 5; // 0이면 자동 새로고침 비활성화 if (refreshInterval === 0) { @@ -933,7 +932,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { clearInterval(intervalId); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataSources]); + }, [dataSources, element?.chartConfig?.refreshInterval]); // 타일맵 URL (chartConfig에서 가져오기) const tileMapUrl = From a491f083376f0501b2974f82d803f1344e033065 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 10:49:11 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=A0=AF?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EB=A7=88=EC=BB=A4=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=20=EC=84=A0=ED=83=9D=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data-sources/MultiDatabaseConfig.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx index bdf6b6b0..9f5d21f3 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx @@ -321,6 +321,29 @@ ORDER BY 하위부서수 DESC`, />
+ {/* 마커 종류 선택 (MapTestWidgetV2 전용) */} +
+ + +

지도에 표시할 마커의 모양을 선택합니다

+
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
From 2eb8c3a61ba35e57f16dcd4396ad84ef8c897494 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 11:16:03 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/WidgetConfigSidebar.tsx | 4 + .../dashboard/widgets/ListTestWidget.tsx | 138 ++++++++++++------ 2 files changed, 97 insertions(+), 45 deletions(-) diff --git a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx index d126d8d9..a18abf9a 100644 --- a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx @@ -352,6 +352,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge console.log("🔧 [WidgetConfigSidebar] handleApply 호출:", { subtype: element.subtype, + isMultiDataSourceWidget, + dataSources, + listConfig, + finalChartConfig, customMetricConfig, updatedElement, }); diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index fdddb234..c46244b1 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -37,8 +37,19 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { - return element?.dataSources || element?.chartConfig?.dataSources; - }, [element?.dataSources, element?.chartConfig?.dataSources]); + // 다중 데이터 소스 우선 + const multiSources = element?.dataSources || element?.chartConfig?.dataSources; + if (multiSources && multiSources.length > 0) { + return multiSources; + } + + // 단일 데이터 소스 fallback (배열로 변환) + if (element?.dataSource) { + return [element.dataSource]; + } + + return []; + }, [element?.dataSources, element?.chartConfig?.dataSources, element?.dataSource]); // // console.log("📊 dataSources 확인:", { // hasDataSources: !!dataSources, @@ -58,6 +69,27 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { cardColumns: 3, }; + // visible 컬럼 설정 객체 배열 (field + label) + const visibleColumnConfigs = useMemo(() => { + if (config.columns && config.columns.length > 0 && typeof config.columns[0] === "object") { + return config.columns.filter((col: any) => col.visible !== false); + } + return []; + }, [config.columns]); + + // 표시할 컬럼 필드명 (데이터 접근용) + const displayColumns = useMemo(() => { + if (!data?.columns) return []; + + // 컬럼 설정이 있으면 field 사용 + if (visibleColumnConfigs.length > 0) { + return visibleColumnConfigs.map((col: any) => col.field); + } + + // 자동 모드: 모든 컬럼 표시 + return data.columns; + }, [data?.columns, visibleColumnConfigs]); + // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { if (!dataSources || dataSources.length === 0) { @@ -313,50 +345,66 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const paginatedRows = data?.rows.slice(startIndex, endIndex) || []; // 테이블 뷰 - const renderTable = () => ( -
- - {config.showHeader && ( - - - {data?.columns.map((col) => ( - - {col} - - ))} - - - )} - - {paginatedRows.map((row, idx) => ( - - {data?.columns.map((col) => ( - - {String(row[col] ?? "")} - - ))} - - ))} - -
-
- ); + const renderTable = () => { + // 헤더명 가져오기 (label 우선, 없으면 field 그대로) + const getHeaderLabel = (field: string) => { + const colConfig = visibleColumnConfigs.find((col: any) => col.field === field); + return colConfig?.label || field; + }; + + return ( +
+ + {config.showHeader && ( + + + {displayColumns.map((field) => ( + + {getHeaderLabel(field)} + + ))} + + + )} + + {paginatedRows.map((row, idx) => ( + + {displayColumns.map((field) => ( + + {String(row[field] ?? "")} + + ))} + + ))} + +
+
+ ); + }; // 카드 뷰 - const renderCards = () => ( -
- {paginatedRows.map((row, idx) => ( - - {data?.columns.map((col) => ( -
- {col}: - {String(row[col] ?? "")} -
- ))} -
- ))} -
- ); + const renderCards = () => { + // 헤더명 가져오기 (label 우선, 없으면 field 그대로) + const getLabel = (field: string) => { + const colConfig = visibleColumnConfigs.find((col: any) => col.field === field); + return colConfig?.label || field; + }; + + return ( +
+ {paginatedRows.map((row, idx) => ( + + {displayColumns.map((field) => ( +
+ {getLabel(field)}: + {String(row[field] ?? "")} +
+ ))} +
+ ))} +
+ ); + }; return (
@@ -396,7 +444,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {

{error}

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

데이터 소스를 연결해주세요 From 05273daa920131768077c867762c554606137be8 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 11:35:16 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=B9=B4=EB=93=9C=20=EC=9C=84=EC=A0=AF=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/CustomMetricTestWidget.tsx | 25 ++++++++----------- .../dashboard/widgets/CustomMetricWidget.tsx | 23 ++++++----------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx index f7d97779..1b78801e 100644 --- a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx @@ -209,10 +209,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (loading) { return ( -

+
-

데이터 로딩 중...

+

데이터 로딩 중...

); @@ -220,12 +220,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (error) { return ( -
+
-

⚠️ {error}

+

⚠️ {error}

@@ -244,10 +244,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 설정이 없으면 안내 화면 if (!hasDataSource || !hasConfig) { return ( -
+
-

통계 카드

-
+

통계 카드

+

📊 단일 통계 위젯

  • • 데이터 소스에서 쿼리를 실행합니다
  • @@ -256,7 +256,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
  • • COUNT, SUM, AVG, MIN, MAX 지원
-
+

⚙️ 설정 방법

1. 데이터 탭에서 쿼리 실행

2. 필터 조건 추가 (선택사항)

@@ -274,7 +274,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 통계 카드 렌더링 (전체 크기 꽉 차게) return ( -
+
{/* 제목 */}
{config?.title || "통계"}
@@ -283,11 +283,6 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg {formattedValue} {config?.unit && {config.unit}}
- - {/* 필터 표시 (디버깅용, 작게) */} - {config?.filters && config.filters.length > 0 && ( -
필터: {config.filters.length}개 적용됨
- )}
); } diff --git a/frontend/components/dashboard/widgets/CustomMetricWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricWidget.tsx index 3f3538b4..fcd5593f 100644 --- a/frontend/components/dashboard/widgets/CustomMetricWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricWidget.tsx @@ -268,23 +268,14 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) // 통계 카드 렌더링 return ( -
-
- {/* 제목 */} -
{config?.title || "통계"}
+
+ {/* 제목 */} +
{config?.title || "통계"}
- {/* 값 */} -
- {formattedValue} - {config?.unit && {config.unit}} -
- - {/* 필터 표시 (디버깅용, 작게) */} - {config?.filters && config.filters.length > 0 && ( -
- 필터: {config.filters.length}개 적용됨 -
- )} + {/* 값 */} +
+ {formattedValue} + {config?.unit && {config.unit}}
); From 02d4a3a3d38d82bd62e792b030d19b79744bba11 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 12:10:10 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=81=AC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9C=84=EC=A0=AF=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/widgets/RiskAlertTestWidget.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 7b27bf69..8aa2e3e2 100644 --- a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx @@ -634,7 +634,26 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp )}

{alert.description}

- {new Date(alert.timestamp).toLocaleString("ko-KR")} + + {(() => { + const ts = String(alert.timestamp); + + // yyyyMMddHHmm 형식 감지 (예: 20251114 1000) + if (/^\d{12}$/.test(ts)) { + const year = ts.substring(0, 4); + const month = ts.substring(4, 6); + const day = ts.substring(6, 8); + const hour = ts.substring(8, 10); + const minute = ts.substring(10, 12); + const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:00`); + return isNaN(date.getTime()) ? ts : date.toLocaleString("ko-KR"); + } + + // ISO 형식 또는 일반 날짜 형식 + const date = new Date(ts); + return isNaN(date.getTime()) ? ts : date.toLocaleString("ko-KR"); + })()} + {alert.source && · {alert.source}}