From d45443521d9dc313a6c0bd7b5bd92aa5832d72b7 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 19 Jan 2026 15:17:28 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=91=EA=B3=84=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80:=20AggregationWidgetComponent=EC=99=80=20AggregationW?= =?UTF-8?q?idgetConfigPanel=EC=97=90=EC=84=9C=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=EC=9D=84=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=98?= =?UTF-8?q?=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80,=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=ED=95=98=EC=97=AC=20=EB=8B=A4=EC=96=91=ED=95=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=86=8C=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=84=ED=84=B0=EB=A7=81=EC=9D=84=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EB=98=90?= =?UTF-8?q?=ED=95=9C,=20=ED=95=84=ED=84=B0=20=EC=97=B0=EC=82=B0=EC=9E=90?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B0=92=20=EC=86=8C=EC=8A=A4=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EA=B2=BD=ED=97=98=EC=9D=84=20=ED=96=A5?= =?UTF-8?q?=EC=83=81=EC=8B=9C=EC=BC=B0=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AggregationWidgetComponent.tsx | 386 ++++++++++-- .../AggregationWidgetConfigPanel.tsx | 554 +++++++++++++++--- .../components/v2-aggregation-widget/index.ts | 23 +- .../components/v2-aggregation-widget/types.ts | 58 +- 4 files changed, 867 insertions(+), 154 deletions(-) diff --git a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx index ce0b0325..77e59474 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx +++ b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx @@ -1,16 +1,99 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo, useCallback } from "react"; import { ComponentRendererProps } from "@/types/component"; -import { AggregationWidgetConfig, AggregationItem, AggregationResult, AggregationType } from "./types"; -import { Calculator, TrendingUp, Hash, ArrowUp, ArrowDown } from "lucide-react"; +import { AggregationWidgetConfig, AggregationItem, AggregationResult, AggregationType, FilterCondition, DataSourceType } from "./types"; +import { Calculator, TrendingUp, Hash, ArrowUp, ArrowDown, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext"; +import { apiClient } from "@/lib/api/client"; interface AggregationWidgetComponentProps extends ComponentRendererProps { config?: AggregationWidgetConfig; // 외부에서 데이터를 직접 전달받을 수 있음 externalData?: any[]; + // 폼 데이터 (필터 조건용) + formData?: Record; + // 선택된 행 데이터 + selectedRows?: any[]; +} + +/** + * 필터 조건을 적용하여 데이터 필터링 + */ +function applyFilters( + data: any[], + filters: FilterCondition[], + filterLogic: "AND" | "OR", + formData: Record, + selectedRows: any[] +): any[] { + if (!filters || filters.length === 0) { + return data; + } + + const enabledFilters = filters.filter((f) => f.enabled && f.columnName); + + if (enabledFilters.length === 0) { + return data; + } + + return data.filter((row) => { + const results = enabledFilters.map((filter) => { + const rowValue = row[filter.columnName]; + + // 값 소스에 따라 비교 값 결정 + let compareValue: any; + switch (filter.valueSourceType) { + case "static": + compareValue = filter.staticValue; + break; + case "formField": + compareValue = formData?.[filter.formFieldName || ""]; + break; + case "selection": + // 선택된 행에서 값 가져오기 (첫 번째 선택 행 기준) + compareValue = selectedRows?.[0]?.[filter.sourceColumnName || ""]; + break; + case "urlParam": + if (typeof window !== "undefined") { + const urlParams = new URLSearchParams(window.location.search); + compareValue = urlParams.get(filter.urlParamName || ""); + } + break; + } + + // 연산자에 따른 비교 + switch (filter.operator) { + case "eq": + return rowValue == compareValue; + case "neq": + return rowValue != compareValue; + case "gt": + return Number(rowValue) > Number(compareValue); + case "gte": + return Number(rowValue) >= Number(compareValue); + case "lt": + return Number(rowValue) < Number(compareValue); + case "lte": + return Number(rowValue) <= Number(compareValue); + case "like": + return String(rowValue || "").toLowerCase().includes(String(compareValue || "").toLowerCase()); + case "in": + const inValues = String(compareValue || "").split(",").map((v) => v.trim()); + return inValues.includes(String(rowValue)); + case "isNull": + return rowValue === null || rowValue === undefined || rowValue === ""; + case "isNotNull": + return rowValue !== null && rowValue !== undefined && rowValue !== ""; + default: + return true; + } + }); + + // AND/OR 논리 적용 + return filterLogic === "AND" ? results.every((r) => r) : results.some((r) => r); + }); } /** @@ -22,12 +105,14 @@ export function AggregationWidgetComponent({ isDesignMode = false, config: propsConfig, externalData, + formData = {}, + selectedRows = [], }: AggregationWidgetComponentProps) { // 다국어 지원 const { getText } = useScreenMultiLang(); const componentConfig: AggregationWidgetConfig = { - dataSourceType: "manual", + dataSourceType: "table", items: [], layout: "horizontal", showLabels: true, @@ -51,6 +136,11 @@ export function AggregationWidgetComponent({ const { dataSourceType, dataSourceComponentId, + tableName, + customTableName, + useCustomTable, + filters, + filterLogic = "AND", items, layout, showLabels, @@ -64,26 +154,169 @@ export function AggregationWidgetComponent({ valueFontSize, labelColor, valueColor, + autoRefresh, + refreshInterval, + refreshOnFormChange, } = componentConfig; // 데이터 상태 const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 사용할 테이블명 결정 + const effectiveTableName = useCustomTable && customTableName ? customTableName : tableName; + + // Refs로 최신 값 참조 (의존성 배열에서 제외하여 무한 루프 방지) + const filtersRef = React.useRef(filters); + const formDataRef = React.useRef(formData); + const selectedRowsRef = React.useRef(selectedRows); + + // 값이 변경될 때마다 ref 업데이트 + React.useEffect(() => { + filtersRef.current = filters; + }, [filters]); + + React.useEffect(() => { + formDataRef.current = formData; + }, [formData]); + + React.useEffect(() => { + selectedRowsRef.current = selectedRows; + }, [selectedRows]); + + // 테이블에서 데이터 조회 (dataSourceType === "table"일 때) + const fetchTableData = useCallback(async () => { + if (isDesignMode || !effectiveTableName || dataSourceType !== "table") { + return; + } + + try { + setLoading(true); + setError(null); + + // 테이블 데이터 조회 API 호출 + // 멀티테넌시: company_code 자동 필터링 활성화 + const response = await apiClient.post(`/table-management/tables/${effectiveTableName}/data`, { + size: 10000, // 집계용이므로 충분한 데이터 조회 + page: 1, + autoFilter: { + enabled: true, + filterColumn: "company_code", + userField: "companyCode", + }, + }); + + // 응답 구조: { success: true, data: { data: [...], total: ... } } + const raw = response.data?.data || response.data; + const rows = raw?.data || raw || []; + + if (Array.isArray(rows)) { + // 필터 적용 + const filteredData = applyFilters( + rows, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); + } + } catch (err: any) { + console.error("집계 위젯 데이터 조회 오류:", err); + setError(err.message || "데이터 조회 실패"); + } finally { + setLoading(false); + } + }, [effectiveTableName, dataSourceType, isDesignMode, filterLogic]); + + // 테이블 데이터 조회 (초기 로드) + useEffect(() => { + if (dataSourceType === "table" && effectiveTableName && !isDesignMode) { + fetchTableData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataSourceType, effectiveTableName, isDesignMode]); + + // 폼 데이터 변경 시 재조회 (refreshOnFormChange가 true일 때) + const formDataKey = JSON.stringify(formData); + useEffect(() => { + if (dataSourceType === "table" && refreshOnFormChange && !isDesignMode && effectiveTableName) { + // 초기 로드 후에만 재조회 + const timeoutId = setTimeout(() => { + fetchTableData(); + }, 100); + return () => clearTimeout(timeoutId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formDataKey, refreshOnFormChange]); + + // 자동 새로고침 + useEffect(() => { + if (dataSourceType === "table" && autoRefresh && refreshInterval && !isDesignMode) { + const interval = setInterval(fetchTableData, refreshInterval * 1000); + return () => clearInterval(interval); + } + }, [dataSourceType, autoRefresh, refreshInterval, isDesignMode, fetchTableData]); + + // 선택된 행 집계 (dataSourceType === "selection"일 때) + const selectedRowsKey = JSON.stringify(selectedRows); + useEffect(() => { + if (dataSourceType === "selection" && Array.isArray(selectedRows) && selectedRows.length > 0) { + setData(selectedRows); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataSourceType, selectedRowsKey]); // 외부 데이터가 있으면 사용 + const externalDataKey = externalData ? JSON.stringify(externalData.slice(0, 5)) : null; // 첫 5개만 비교 useEffect(() => { if (externalData && Array.isArray(externalData)) { - setData(externalData); + // 필터 적용 + const filteredData = applyFilters( + externalData, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); } - }, [externalData]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [externalDataKey, filterLogic]); - // 컴포넌트 데이터 변경 이벤트 리스닝 + // 컴포넌트 데이터 변경 이벤트 리스닝 (dataSourceType === "component"일 때) useEffect(() => { - if (!dataSourceComponentId || isDesignMode) return; + if (dataSourceType !== "component" || !dataSourceComponentId || isDesignMode) return; const handleDataChange = (event: CustomEvent) => { const { componentId, data: eventData } = event.detail || {}; if (componentId === dataSourceComponentId && Array.isArray(eventData)) { - setData(eventData); + // 필터 적용 + const filteredData = applyFilters( + eventData, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); + } + }; + + // 선택 변경 이벤트 (체크박스 선택 등) + const handleSelectionChange = (event: CustomEvent) => { + const { componentId, selectedData } = event.detail || {}; + if (componentId === dataSourceComponentId && Array.isArray(selectedData)) { + // 선택된 데이터만 집계 + const filteredData = applyFilters( + selectedData, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); } }; @@ -91,12 +324,15 @@ export function AggregationWidgetComponent({ window.addEventListener("repeaterDataChange" as any, handleDataChange); // 테이블 리스트 데이터 변경 이벤트 window.addEventListener("tableListDataChange" as any, handleDataChange); + // 선택 변경 이벤트 + window.addEventListener("selectionChange" as any, handleSelectionChange); return () => { window.removeEventListener("repeaterDataChange" as any, handleDataChange); window.removeEventListener("tableListDataChange" as any, handleDataChange); + window.removeEventListener("selectionChange" as any, handleSelectionChange); }; - }, [dataSourceComponentId, isDesignMode]); + }, [dataSourceType, dataSourceComponentId, isDesignMode, filterLogic]); // 집계 계산 const aggregationResults = useMemo((): AggregationResult[] => { @@ -108,10 +344,12 @@ export function AggregationWidgetComponent({ const values = data .map((row) => { const val = row[item.columnName]; - return typeof val === "number" ? val : parseFloat(val) || 0; + const parsed = typeof val === "number" ? val : parseFloat(val) || 0; + return parsed; }) .filter((v) => !isNaN(v)); + let value: number = 0; switch (item.type) { @@ -192,6 +430,20 @@ export function AggregationWidgetComponent({ } }; + // 데이터 소스 타입 라벨 + const getDataSourceLabel = (type: DataSourceType) => { + switch (type) { + case "table": + return "테이블"; + case "component": + return "컴포넌트"; + case "selection": + return "선택 데이터"; + default: + return "수동"; + } + }; + // 디자인 모드 미리보기 if (isDesignMode) { const previewItems: AggregationResult[] = @@ -210,46 +462,80 @@ export function AggregationWidgetComponent({ ]; return ( -
- {previewItems.map((result, index) => ( -
- {showIcons && ( - {getIcon(result.type)} - )} - {showLabels && ( - - {result.label} ({getTypeLabel(result.type)}): - - )} - + {/* 디자인 모드에서 데이터 소스 표시 */} +
+ [{getDataSourceLabel(dataSourceType)}] + {dataSourceType === "table" && effectiveTableName && ( + {effectiveTableName} + )} + {dataSourceType === "component" && dataSourceComponentId && ( + {dataSourceComponentId} + )} + {(filters || []).length > 0 && ( + 필터 {filters?.length}개 + )} +
+
+ {previewItems.map((result, index) => ( +
- {result.formattedValue} - -
- ))} + {showIcons && ( + {getIcon(result.type)} + )} + {showLabels && ( + + {result.label} ({getTypeLabel(result.type)}): + + )} + + {result.formattedValue} + +
+ ))} +
+
+ ); + } + + // 로딩 상태 + if (loading) { + return ( +
+ + 데이터 집계 중... +
+ ); + } + + // 에러 상태 + if (error) { + return ( +
+ {error}
); } diff --git a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx index ebfff828..4bc5020d 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx @@ -14,9 +14,10 @@ import { } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; -import { Plus, Trash2, GripVertical, Database, Table2, ChevronsUpDown, Check } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Trash2, GripVertical, Database, Table2, ChevronsUpDown, Check, Filter, Link2, MousePointer } from "lucide-react"; import { cn } from "@/lib/utils"; -import { AggregationWidgetConfig, AggregationItem, AggregationType } from "./types"; +import { AggregationWidgetConfig, AggregationItem, AggregationType, DataSourceType, FilterCondition, FilterOperator, FilterValueSourceType } from "./types"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { tableTypeApi } from "@/lib/api/screen"; @@ -24,15 +25,40 @@ interface AggregationWidgetConfigPanelProps { config: AggregationWidgetConfig; onChange: (config: Partial) => void; screenTableName?: string; + // 화면 내 컴포넌트 목록 (컴포넌트 연결용) + screenComponents?: Array<{ id: string; componentType: string; label?: string }>; } /** * 집계 위젯 설정 패널 */ +// 연산자 라벨 +const OPERATOR_LABELS: Record = { + eq: "같음 (=)", + neq: "같지 않음 (!=)", + gt: "보다 큼 (>)", + gte: "크거나 같음 (>=)", + lt: "보다 작음 (<)", + lte: "작거나 같음 (<=)", + like: "포함", + in: "목록에 포함", + isNull: "NULL", + isNotNull: "NOT NULL", +}; + +// 값 소스 타입 라벨 +const VALUE_SOURCE_LABELS: Record = { + static: "고정 값", + formField: "폼 필드", + selection: "선택된 행", + urlParam: "URL 파라미터", +}; + export function AggregationWidgetConfigPanel({ config, onChange, screenTableName, + screenComponents = [], }: AggregationWidgetConfigPanelProps) { const [columns, setColumns] = useState>([]); const [loadingColumns, setLoadingColumns] = useState(false); @@ -40,6 +66,9 @@ export function AggregationWidgetConfigPanel({ const [loadingTables, setLoadingTables] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); + // 데이터 소스 타입 (기본값: table) + const dataSourceType = config.dataSourceType || "table"; + // 실제 사용할 테이블 이름 계산 const targetTableName = useMemo(() => { if (config.useCustomTable && config.customTableName) { @@ -156,116 +185,449 @@ export function AggregationWidgetConfigPanel({ ); }); + // 필터 추가 + const addFilter = () => { + const newFilter: FilterCondition = { + id: `filter-${Date.now()}`, + columnName: "", + operator: "eq", + valueSourceType: "static", + staticValue: "", + enabled: true, + }; + onChange({ + filters: [...(config.filters || []), newFilter], + }); + }; + + // 필터 삭제 + const removeFilter = (id: string) => { + onChange({ + filters: (config.filters || []).filter((f) => f.id !== id), + }); + }; + + // 필터 업데이트 + const updateFilter = (id: string, updates: Partial) => { + onChange({ + filters: (config.filters || []).map((f) => + f.id === id ? { ...f, ...updates } : f + ), + }); + }; + + // 연결 가능한 컴포넌트 (리피터, 테이블리스트) + const linkableComponents = screenComponents.filter( + (c) => c.componentType === "v2-unified-repeater" || + c.componentType === "v2-table-list" || + c.componentType === "unified-repeater" || + c.componentType === "table-list" + ); + return (
집계 위젯 설정
- {/* 테이블 설정 (컴포넌트 개발 가이드 준수) */} + {/* 데이터 소스 타입 선택 */}
-

데이터 소스 테이블

-

집계할 데이터의 테이블을 선택합니다

+

데이터 소스

+

집계할 데이터를 가져올 방식을 선택합니다


- {/* 현재 선택된 테이블 표시 (카드 형태) */} -
- -
-
- {config.customTableName || config.tableName || screenTableName || "테이블 미선택"} -
-
- {config.useCustomTable ? "커스텀 테이블" : "화면 기본 테이블"} -
-
+
+ + +
- {/* 테이블 선택 Combobox */} - - - + + + + + + 테이블을 찾을 수 없습니다 + + {/* 그룹 1: 화면 기본 테이블 */} + {screenTableName && ( + + { + onChange({ + useCustomTable: false, + customTableName: undefined, + tableName: screenTableName, + items: [], + filters: [], + }); + setTableComboboxOpen(false); + }} + className="text-xs cursor-pointer" + > + + + {screenTableName} + + + )} + + {/* 그룹 2: 전체 테이블 */} + + {availableTables + .filter((table) => table.tableName !== screenTableName) + .map((table) => ( + { + onChange({ + useCustomTable: true, + customTableName: table.tableName, + tableName: table.tableName, + items: [], + filters: [], + }); + setTableComboboxOpen(false); + }} + className="text-xs cursor-pointer" + > + + + {table.displayName || table.tableName} + + ))} + + + + + +
+ )} + + {/* 컴포넌트 연결 (component 타입일 때) */} + {dataSourceType === "component" && ( +
+ + +

+ 리피터 또는 테이블리스트의 데이터를 집계합니다 +

+
+ )} - {/* 그룹 2: 전체 테이블 */} - - {availableTables - .filter((table) => table.tableName !== screenTableName) - .map((table) => ( - { - onChange({ - useCustomTable: true, - customTableName: table.tableName, - tableName: table.tableName, - items: [], // 테이블 변경 시 집계 항목 초기화 - }); - setTableComboboxOpen(false); - }} - className="text-xs cursor-pointer" - > - - - {table.displayName || table.tableName} - - ))} - - - - - + {/* 선택 데이터 설명 (selection 타입일 때) */} + {dataSourceType === "selection" && ( +
+
+

선택된 행 집계

+

+ 화면에서 사용자가 선택(체크)한 행들만 집계합니다. + 테이블리스트나 리피터에서 선택된 데이터가 자동으로 집계됩니다. +

+
+ + {/* 테이블 선택 (어느 테이블의 선택 데이터인지) */} + +
+ +
+
+ {config.customTableName || config.tableName || screenTableName || "테이블 미선택"} +
+
+
+
+ )}
+ {/* 필터 조건 (table 또는 selection 타입일 때) */} + {(dataSourceType === "table" || dataSourceType === "selection") && ( +
+
+
+

+ + 필터 조건 +

+

집계 대상을 필터링합니다

+
+ +
+
+ + {/* 필터 결합 방식 */} + {(config.filters || []).length > 1 && ( +
+ + +
+ )} + + {/* 필터 목록 */} + {(config.filters || []).length === 0 ? ( +
+ 필터 조건이 없습니다. 전체 데이터를 집계합니다. +
+ ) : ( +
+ {(config.filters || []).map((filter, index) => ( +
+
+
+ updateFilter(filter.id, { enabled: checked as boolean })} + /> + 필터 {index + 1} + {filter.columnName && ( + + {filter.columnName} + + )} +
+ +
+ +
+ {/* 컬럼 선택 */} +
+ + +
+ + {/* 연산자 선택 */} +
+ + +
+ + {/* 값 소스 타입 */} + {filter.operator !== "isNull" && filter.operator !== "isNotNull" && ( + <> +
+ + +
+ + {/* 값 입력 (소스 타입에 따라) */} +
+ + {filter.valueSourceType === "static" && ( + updateFilter(filter.id, { staticValue: e.target.value })} + placeholder="값 입력" + className="h-7 text-xs" + /> + )} + {filter.valueSourceType === "formField" && ( + updateFilter(filter.id, { formFieldName: e.target.value })} + placeholder="필드명 입력" + className="h-7 text-xs" + /> + )} + {filter.valueSourceType === "selection" && ( + + )} + {filter.valueSourceType === "urlParam" && ( + updateFilter(filter.id, { urlParamName: e.target.value })} + placeholder="파라미터명" + className="h-7 text-xs" + /> + )} +
+ + )} +
+
+ ))} +
+ )} +
+ )} + {/* 레이아웃 설정 */}
diff --git a/frontend/lib/registry/components/v2-aggregation-widget/index.ts b/frontend/lib/registry/components/v2-aggregation-widget/index.ts index ab47c1db..75eb0e52 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/index.ts +++ b/frontend/lib/registry/components/v2-aggregation-widget/index.ts @@ -14,13 +14,15 @@ export const V2AggregationWidgetDefinition = createComponentDefinition({ id: "v2-aggregation-widget", name: "집계 위젯", nameEng: "Aggregation Widget", - description: "데이터의 합계, 평균, 개수 등 집계 결과를 표시하는 위젯", + description: "데이터의 합계, 평균, 개수 등 집계 결과를 표시하는 위젯 (필터링 지원)", category: ComponentCategory.DISPLAY, webType: "text", component: AggregationWidgetWrapper, defaultConfig: { - dataSourceType: "manual", + dataSourceType: "table", // 기본값: 테이블에서 직접 조회 items: [], + filters: [], // 필터 조건 + filterLogic: "AND", layout: "horizontal", showLabels: true, showIcons: true, @@ -28,15 +30,26 @@ export const V2AggregationWidgetDefinition = createComponentDefinition({ backgroundColor: "#f8fafc", borderRadius: "6px", padding: "12px", + autoRefresh: false, + refreshOnFormChange: true, // 폼 변경 시 자동 새로고침 } as Partial, defaultSize: { width: 400, height: 60 }, configPanel: AggregationWidgetConfigPanel, icon: "Calculator", - tags: ["집계", "합계", "평균", "개수", "통계", "데이터"], - version: "1.0.0", + tags: ["집계", "합계", "평균", "개수", "통계", "데이터", "필터"], + version: "1.1.0", author: "개발팀", }); // 타입 내보내기 -export type { AggregationWidgetConfig, AggregationItem, AggregationType, AggregationResult } from "./types"; +export type { + AggregationWidgetConfig, + AggregationItem, + AggregationType, + AggregationResult, + DataSourceType, + FilterCondition, + FilterOperator, + FilterValueSourceType, +} from "./types"; diff --git a/frontend/lib/registry/components/v2-aggregation-widget/types.ts b/frontend/lib/registry/components/v2-aggregation-widget/types.ts index 6d480599..b306cb76 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/types.ts +++ b/frontend/lib/registry/components/v2-aggregation-widget/types.ts @@ -5,6 +5,50 @@ import { ComponentConfig } from "@/types/component"; */ export type AggregationType = "sum" | "avg" | "count" | "max" | "min"; +/** + * 데이터 소스 타입 + */ +export type DataSourceType = "table" | "component" | "selection"; + +/** + * 필터 연산자 + */ +export type FilterOperator = + | "eq" // 같음 (=) + | "neq" // 같지 않음 (!=) + | "gt" // 보다 큼 (>) + | "gte" // 크거나 같음 (>=) + | "lt" // 보다 작음 (<) + | "lte" // 작거나 같음 (<=) + | "like" // 포함 (LIKE) + | "in" // 목록에 포함 (IN) + | "isNull" // NULL 여부 + | "isNotNull"; // NOT NULL 여부 + +/** + * 필터 값 소스 타입 + */ +export type FilterValueSourceType = + | "static" // 고정 값 + | "formField" // 폼 필드에서 가져오기 + | "selection" // 선택된 행에서 가져오기 + | "urlParam"; // URL 파라미터에서 가져오기 + +/** + * 필터 조건 + */ +export interface FilterCondition { + id: string; + columnName: string; // 필터 적용할 컬럼 + operator: FilterOperator; // 연산자 + valueSourceType: FilterValueSourceType; // 값 소스 타입 + staticValue?: string | number | boolean; // 고정 값 (valueSourceType이 static일 때) + formFieldName?: string; // 폼 필드명 (valueSourceType이 formField일 때) + sourceColumnName?: string; // 소스 컬럼명 (valueSourceType이 selection일 때) + urlParamName?: string; // URL 파라미터명 (valueSourceType이 urlParam일 때) + enabled: boolean; // 필터 활성화 여부 +} + /** * 개별 집계 항목 설정 */ @@ -26,14 +70,18 @@ export interface AggregationItem { */ export interface AggregationWidgetConfig extends ComponentConfig { // 데이터 소스 설정 - dataSourceType: "repeater" | "tableList" | "manual"; // 데이터 소스 타입 - dataSourceComponentId?: string; // 연결할 컴포넌트 ID (repeater 또는 tableList) + dataSourceType: DataSourceType; // 데이터 소스 타입 + dataSourceComponentId?: string; // 연결할 컴포넌트 ID (component 타입일 때) // 컴포넌트별 테이블 설정 (개발 가이드 준수) tableName?: string; // 사용할 테이블명 customTableName?: string; // 커스텀 테이블명 useCustomTable?: boolean; // true: customTableName 사용 + // 필터 조건 (table 타입일 때 사용) + filters?: FilterCondition[]; + filterLogic?: "AND" | "OR"; // 필터 조건 결합 방식 (기본: AND) + // 집계 항목들 items: AggregationItem[]; @@ -52,6 +100,11 @@ export interface AggregationWidgetConfig extends ComponentConfig { valueFontSize?: string; labelColor?: string; valueColor?: string; + + // 자동 새로고침 설정 + autoRefresh?: boolean; // 자동 새로고침 활성화 + refreshInterval?: number; // 새로고침 간격 (초) + refreshOnFormChange?: boolean; // 폼 데이터 변경 시 새로고침 } /** @@ -64,4 +117,3 @@ export interface AggregationResult { formattedValue: string; type: AggregationType; } -