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({