카드디스플레이 검색필터 구현

This commit is contained in:
kjs
2025-12-03 18:48:23 +09:00
parent 676ec16879
commit cb0bbd1ff3
3 changed files with 340 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useState, useMemo, useCallback } from "react";
import React, { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { ComponentRendererProps } from "@/types/component";
import { CardDisplayConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen";
@@ -13,6 +13,8 @@ import { Badge } from "@/components/ui/badge";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
import { useModalDataStore } from "@/stores/modalDataStore";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { TableFilter, ColumnVisibility, TableColumn } from "@/types/table-options";
export interface CardDisplayComponentProps extends ComponentRendererProps {
config?: CardDisplayConfig;
@@ -48,11 +50,32 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
const splitPanelContext = useSplitPanelContext();
const splitPanelPosition = screenContext?.splitPanelPosition;
// TableOptions Context (검색 필터 위젯 연동용)
let tableOptionsContext: ReturnType<typeof useTableOptions> | null = null;
try {
tableOptionsContext = useTableOptions();
} catch (e) {
// Context가 없으면 (디자이너 모드) 무시
}
// 테이블 데이터 상태 관리
const [loadedTableData, setLoadedTableData] = useState<any[]>([]);
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 필터 상태 (검색 필터 위젯에서 전달받은 필터)
const [filters, setFiltersInternal] = useState<TableFilter[]>([]);
// 필터 상태 변경 래퍼 (로깅용)
const setFilters = useCallback((newFilters: TableFilter[]) => {
console.log("🎴 [CardDisplay] setFilters 호출됨:", {
componentId: component.id,
filtersCount: newFilters.length,
filters: newFilters,
});
setFiltersInternal(newFilters);
}, [component.id]);
// 카테고리 매핑 상태 (카테고리 코드 -> 라벨/색상)
const [columnMeta, setColumnMeta] = useState<
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
@@ -380,6 +403,195 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
}
}, [screenContext, component.id, dataProvider]);
// TableOptionsContext에 테이블 등록 (검색 필터 위젯 연동용)
const tableId = `card-display-${component.id}`;
const tableNameToUse = tableName || component.componentConfig?.tableName || '';
const tableLabel = component.componentConfig?.title || component.label || "카드 디스플레이";
// ref로 최신 데이터 참조 (useCallback 의존성 문제 해결)
const loadedTableDataRef = useRef(loadedTableData);
const categoryMappingsRef = useRef(categoryMappings);
useEffect(() => {
loadedTableDataRef.current = loadedTableData;
}, [loadedTableData]);
useEffect(() => {
categoryMappingsRef.current = categoryMappings;
}, [categoryMappings]);
// 필터가 변경되면 데이터 다시 로드 (테이블 리스트와 동일한 패턴)
// 초기 로드 여부 추적
const isInitialLoadRef = useRef(true);
useEffect(() => {
if (!tableNameToUse || isDesignMode) return;
// 초기 로드는 별도 useEffect에서 처리하므로 스킵
if (isInitialLoadRef.current) {
isInitialLoadRef.current = false;
return;
}
const loadFilteredData = async () => {
try {
setLoading(true);
// 필터 값을 검색 파라미터로 변환
const searchParams: Record<string, any> = {};
filters.forEach(filter => {
if (filter.value !== undefined && filter.value !== null && filter.value !== '') {
searchParams[filter.columnName] = filter.value;
}
});
console.log("🔍 [CardDisplay] 필터 적용 데이터 로드:", {
tableName: tableNameToUse,
filtersCount: filters.length,
searchParams,
});
// search 파라미터로 검색 조건 전달 (API 스펙에 맞게)
const dataResponse = await tableTypeApi.getTableData(tableNameToUse, {
page: 1,
size: 50,
search: searchParams,
});
setLoadedTableData(dataResponse.data);
// 데이터 건수 업데이트
if (tableOptionsContext) {
tableOptionsContext.updateTableDataCount(tableId, dataResponse.data?.length || 0);
}
} catch (error) {
console.error("❌ [CardDisplay] 필터 적용 실패:", error);
} finally {
setLoading(false);
}
};
// 필터 변경 시 항상 데이터 다시 로드 (빈 필터 = 전체 데이터)
loadFilteredData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters, tableNameToUse, isDesignMode, tableId]);
// 컬럼 고유 값 조회 함수 (select 타입 필터용)
const getColumnUniqueValues = useCallback(async (columnName: string): Promise<Array<{ label: string; value: string }>> => {
if (!tableNameToUse) return [];
try {
// 현재 로드된 데이터에서 고유 값 추출
const uniqueValues = new Set<string>();
loadedTableDataRef.current.forEach(row => {
const value = row[columnName];
if (value !== null && value !== undefined && value !== '') {
uniqueValues.add(String(value));
}
});
// 카테고리 매핑이 있으면 라벨 적용
const mapping = categoryMappingsRef.current[columnName];
return Array.from(uniqueValues).map(value => ({
value,
label: mapping?.[value]?.label || value,
}));
} catch (error) {
console.error(`❌ [CardDisplay] 고유 값 조회 실패: ${columnName}`, error);
return [];
}
}, [tableNameToUse]);
// TableOptionsContext에 등록
// registerTable과 unregisterTable 함수 참조 저장 (의존성 안정화)
const registerTableRef = useRef(tableOptionsContext?.registerTable);
const unregisterTableRef = useRef(tableOptionsContext?.unregisterTable);
// setFiltersInternal을 ref로 저장 (등록 시 최신 함수 사용)
const setFiltersRef = useRef(setFiltersInternal);
const getColumnUniqueValuesRef = useRef(getColumnUniqueValues);
useEffect(() => {
registerTableRef.current = tableOptionsContext?.registerTable;
unregisterTableRef.current = tableOptionsContext?.unregisterTable;
}, [tableOptionsContext]);
useEffect(() => {
setFiltersRef.current = setFiltersInternal;
}, [setFiltersInternal]);
useEffect(() => {
getColumnUniqueValuesRef.current = getColumnUniqueValues;
}, [getColumnUniqueValues]);
// 테이블 등록 (한 번만 실행, 컬럼 변경 시에만 재등록)
const columnsKey = JSON.stringify(loadedTableColumns.map((col: any) => col.columnName || col.column_name));
useEffect(() => {
if (!registerTableRef.current || !unregisterTableRef.current) return;
if (isDesignMode || !tableNameToUse || loadedTableColumns.length === 0) return;
// 컬럼 정보를 TableColumn 형식으로 변환
const columns: TableColumn[] = loadedTableColumns.map((col: any) => ({
columnName: col.columnName || col.column_name,
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
inputType: columnMeta[col.columnName || col.column_name]?.inputType || 'text',
visible: true,
width: 200,
sortable: true,
filterable: true,
}));
// onFilterChange는 ref를 통해 최신 함수를 호출하는 래퍼 사용
const onFilterChangeWrapper = (newFilters: TableFilter[]) => {
console.log("🎴 [CardDisplay] onFilterChange 래퍼 호출:", {
tableId,
filtersCount: newFilters.length,
});
setFiltersRef.current(newFilters);
};
const getColumnUniqueValuesWrapper = async (columnName: string) => {
return getColumnUniqueValuesRef.current(columnName);
};
const registration = {
tableId,
label: tableLabel,
tableName: tableNameToUse,
columns,
dataCount: loadedTableData.length,
onFilterChange: onFilterChangeWrapper,
onGroupChange: () => {}, // 카드 디스플레이는 그룹핑 미지원
onColumnVisibilityChange: () => {}, // 카드 디스플레이는 컬럼 가시성 미지원
getColumnUniqueValues: getColumnUniqueValuesWrapper,
};
console.log("📋 [CardDisplay] TableOptionsContext에 등록:", {
tableId,
tableName: tableNameToUse,
columnsCount: columns.length,
dataCount: loadedTableData.length,
});
registerTableRef.current(registration);
const unregister = unregisterTableRef.current;
const currentTableId = tableId;
return () => {
console.log("📋 [CardDisplay] TableOptionsContext에서 해제:", currentTableId);
unregister(currentTableId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isDesignMode,
tableId,
tableNameToUse,
tableLabel,
columnsKey, // 컬럼 변경 시에만 재등록
]);
// 로딩 중인 경우 로딩 표시
if (loading) {
return (