From e785dbbe6eeb596c23541d1740a98bf826fbc177 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 19 Jan 2026 15:31:01 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=91=EA=B3=84=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80:=20Aggregat?= =?UTF-8?q?ionWidgetComponent=EC=97=90=EC=84=9C=20=EB=8B=A4=EC=96=91?= =?UTF-8?q?=ED=95=9C=20=EC=84=A0=ED=83=9D=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=EC=8B=A0=ED=95=98=EC=97=AC=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=EB=90=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=84=A4=EC=A0=95=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=98=EC=98=80?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EB=98=90=ED=95=9C,=20Aggregatio?= =?UTF-8?q?nWidgetConfigPanel=EC=97=90=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=92=20=EC=BD=A4=EB=B3=B4=EB=B0=95=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=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=EC=97=90=EA=B2=8C=20=EB=8D=94=20=EB=82=98?= =?UTF-8?q?=EC=9D=80=20=EC=84=A0=ED=83=9D=20=EA=B2=BD=ED=97=98=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B3=B5=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20?= =?UTF-8?q?=EC=9D=B4=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=86=8C=EC=8A=A4=20=ED=83=80=EC=9E=85=EC=9D=B4=20?= =?UTF-8?q?"selection"=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=EC=9D=98=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=B4=20=EA=B0=95=ED=99=94=EB=90=98=EC=97=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AggregationWidgetComponent.tsx | 77 ++++++++ .../AggregationWidgetConfigPanel.tsx | 187 ++++++++++++++++-- 2 files changed, 248 insertions(+), 16 deletions(-) diff --git a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx index 77e59474..bab8e691 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx +++ b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx @@ -260,6 +260,7 @@ export function AggregationWidgetComponent({ }, [dataSourceType, autoRefresh, refreshInterval, isDesignMode, fetchTableData]); // 선택된 행 집계 (dataSourceType === "selection"일 때) + // props로 전달된 selectedRows 사용 const selectedRowsKey = JSON.stringify(selectedRows); useEffect(() => { if (dataSourceType === "selection" && Array.isArray(selectedRows) && selectedRows.length > 0) { @@ -268,6 +269,82 @@ export function AggregationWidgetComponent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataSourceType, selectedRowsKey]); + // 전역 선택 이벤트 수신 (dataSourceType === "selection"일 때) + useEffect(() => { + if (dataSourceType !== "selection" || isDesignMode) return; + + // 테이블리스트에서 발생하는 선택 이벤트 수신 + // tableListDataChange 이벤트의 data가 선택된 행들임 + const handleTableListDataChange = (event: CustomEvent) => { + const { data: eventData, selectedRows: eventSelectedRows } = event.detail || {}; + // data가 선택된 행 데이터 배열 + const rows = eventData || []; + + if (Array.isArray(rows)) { + // 필터 적용 + const filteredData = applyFilters( + rows, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); + } + }; + + // 리피터에서 발생하는 이벤트 + const handleRepeaterDataChange = (event: CustomEvent) => { + const { data: eventData, selectedData } = event.detail || {}; + const rows = selectedData || eventData || []; + + if (Array.isArray(rows)) { + const filteredData = applyFilters( + rows, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); + } + }; + + // 일반 선택 이벤트 + const handleSelectionChange = (event: CustomEvent) => { + const { selectedRows: eventSelectedRows, selectedData, checkedRows, selectedItems } = event.detail || {}; + const rows = selectedData || eventSelectedRows || checkedRows || selectedItems || []; + + if (Array.isArray(rows)) { + const filteredData = applyFilters( + rows, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); + } + }; + + // 다양한 선택 이벤트 수신 + window.addEventListener("tableListDataChange" as any, handleTableListDataChange); + window.addEventListener("repeaterDataChange" as any, handleRepeaterDataChange); + window.addEventListener("selectionChange" as any, handleSelectionChange); + window.addEventListener("tableSelectionChange" as any, handleSelectionChange); + window.addEventListener("rowSelectionChange" as any, handleSelectionChange); + window.addEventListener("checkboxSelectionChange" as any, handleSelectionChange); + + return () => { + window.removeEventListener("tableListDataChange" as any, handleTableListDataChange); + window.removeEventListener("repeaterDataChange" as any, handleRepeaterDataChange); + window.removeEventListener("selectionChange" as any, handleSelectionChange); + window.removeEventListener("tableSelectionChange" as any, handleSelectionChange); + window.removeEventListener("rowSelectionChange" as any, handleSelectionChange); + window.removeEventListener("checkboxSelectionChange" as any, handleSelectionChange); + }; + }, [dataSourceType, isDesignMode, filterLogic]); + // 외부 데이터가 있으면 사용 const externalDataKey = externalData ? JSON.stringify(externalData.slice(0, 5)) : null; // 첫 5개만 비교 useEffect(() => { diff --git a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx index 4bc5020d..bcdc1e32 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx @@ -20,6 +20,7 @@ import { cn } from "@/lib/utils"; import { AggregationWidgetConfig, AggregationItem, AggregationType, DataSourceType, FilterCondition, FilterOperator, FilterValueSourceType } from "./types"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { tableTypeApi } from "@/lib/api/screen"; +import { getCategoryValues } from "@/lib/api/tableCategoryValue"; interface AggregationWidgetConfigPanelProps { config: AggregationWidgetConfig; @@ -29,6 +30,74 @@ interface AggregationWidgetConfigPanelProps { screenComponents?: Array<{ id: string; componentType: string; label?: string }>; } +/** + * 카테고리 값 콤보박스 컴포넌트 + */ +function CategoryValueCombobox({ + value, + options, + onChange, + placeholder = "값 선택", +}: { + value: string; + options: Array<{ value: string; label: string }>; + onChange: (value: string) => void; + placeholder?: string; +}) { + const [open, setOpen] = useState(false); + + const selectedOption = options.find((opt) => opt.value === value); + + return ( + + + + + + + + + + 결과 없음 + + + {options.map((opt, index) => ( + { + onChange(opt.value); + setOpen(false); + }} + className="text-xs cursor-pointer" + > + + {opt.label} + + ))} + + + + + + ); +} + /** * 집계 위젯 설정 패널 */ @@ -60,11 +129,14 @@ export function AggregationWidgetConfigPanel({ screenTableName, screenComponents = [], }: AggregationWidgetConfigPanelProps) { - const [columns, setColumns] = useState>([]); + const [columns, setColumns] = useState>([]); const [loadingColumns, setLoadingColumns] = useState(false); const [availableTables, setAvailableTables] = useState>([]); const [loadingTables, setLoadingTables] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); + + // 카테고리 옵션 캐시 (categoryCode -> options) + const [categoryOptionsCache, setCategoryOptionsCache] = useState>>({}); // 데이터 소스 타입 (기본값: table) const dataSourceType = config.dataSourceType || "table"; @@ -117,15 +189,23 @@ export function AggregationWidgetConfigPanel({ try { const result = await tableManagementApi.getColumnList(targetTableName); if (result.success && result.data?.columns) { - setColumns( - result.data.columns.map((col: any) => ({ - columnName: col.columnName || col.column_name, - label: col.displayName || col.columnLabel || col.column_label || col.label || col.columnName || col.column_name, - dataType: col.dataType || col.data_type, - inputType: col.inputType || col.input_type, - webType: col.webType || col.web_type, - })) + const mappedColumns = result.data.columns.map((col: any) => ({ + columnName: col.columnName || col.column_name, + label: col.displayName || col.columnLabel || col.column_label || col.label || col.columnName || col.column_name, + dataType: col.dataType || col.data_type, + inputType: col.inputType || col.input_type, + webType: col.webType || col.web_type, + categoryCode: col.categoryCode || col.category_code, + })); + setColumns(mappedColumns); + + // 카테고리 타입 컬럼의 옵션 로드 + const categoryColumns = mappedColumns.filter( + (col: any) => col.inputType === "category" || col.webType === "category" ); + if (categoryColumns.length > 0) { + loadCategoryOptions(categoryColumns); + } } else { setColumns([]); } @@ -140,6 +220,63 @@ export function AggregationWidgetConfigPanel({ loadColumns(); }, [targetTableName]); + // 카테고리 옵션 로드 함수 + const loadCategoryOptions = async (categoryColumns: Array<{ columnName: string; categoryCode?: string }>) => { + if (!targetTableName) return; + + const newCache: Record> = { ...categoryOptionsCache }; + + for (const col of categoryColumns) { + const cacheKey = `${targetTableName}_${col.columnName}`; + if (newCache[cacheKey]) continue; + + try { + // 카테고리 API 호출 + const result = await getCategoryValues(targetTableName, col.columnName, false); + if (result.success && Array.isArray(result.data)) { + // 중복 제거 (valueCode 기준) + const seenCodes = new Set(); + const uniqueOptions: Array<{ value: string; label: string }> = []; + + for (const item of result.data) { + const code = item.valueCode || item.code || item.value || item.id; + if (!seenCodes.has(code)) { + seenCodes.add(code); + uniqueOptions.push({ + value: code, + // valueLabel이 실제 표시명 + label: item.valueLabel || item.valueName || item.name || item.label || item.displayName || code, + }); + } + } + + newCache[cacheKey] = uniqueOptions; + } else { + newCache[cacheKey] = []; + } + } catch (error) { + console.error(`카테고리 옵션 로드 실패 (${col.columnName}):`, error); + // 실패해도 빈 배열로 캐시 + newCache[cacheKey] = []; + } + } + + setCategoryOptionsCache(newCache); + }; + + // 컬럼의 카테고리 옵션 가져오기 + const getCategoryOptionsForColumn = (columnName: string): Array<{ value: string; label: string }> => { + if (!targetTableName) return []; + const cacheKey = `${targetTableName}_${columnName}`; + return categoryOptionsCache[cacheKey] || []; + }; + + // 컬럼이 카테고리 타입인지 확인 + const isCategoryColumn = (columnName: string): boolean => { + const column = columns.find((c) => c.columnName === columnName); + return column?.inputType === "category" || column?.webType === "category"; + }; + // 집계 항목 추가 const addItem = () => { const newItem: AggregationItem = { @@ -510,7 +647,14 @@ export function AggregationWidgetConfigPanel({ updateFilter(filter.id, { staticValue: e.target.value })} - placeholder="값 입력" - className="h-7 text-xs" - /> + isCategoryColumn(filter.columnName) ? ( + // 카테고리 타입일 때 콤보박스 (검색 가능) + updateFilter(filter.id, { staticValue: value })} + placeholder="값 선택" + /> + ) : ( + // 일반 타입일 때 입력 필드 + updateFilter(filter.id, { staticValue: e.target.value })} + placeholder="값 입력" + className="h-7 text-xs" + /> + ) )} {filter.valueSourceType === "formField" && (