From 297870a24c46b89ab4ddb7cf78c018e92822d60e Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 3 Nov 2025 13:59:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20TableListComponent=EC=97=90=20FlowWidge?= =?UTF-8?q?t=EA=B3=BC=20=EB=8F=99=EC=9D=BC=ED=95=9C=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 선택/해제 기능 추가 - 선택된 컬럼 개수 표시 추가 - 필터 설정 localStorage 저장/로드 기능 - 체크된 항목만 실제 검색 필터로 표시 - 저장 시 Toast 알림 추가 - FlowWidget과 완전히 동일한 UI/UX 적용 --- .../screen/InteractiveDataTable.tsx | 60 ++++++++- .../table-list/TableListComponent.tsx | 126 +++++++++++++----- 2 files changed, 153 insertions(+), 33 deletions(-) diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index fa90bd2d..e05cc973 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -38,10 +38,12 @@ import { Folder, FolderOpen, Grid, + Filter, } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; import { getCurrentUser, UserInfo } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { cn } from "@/lib/utils"; import { downloadFile, getLinkedFiles, getFilePreviewUrl, getDirectFileUrl } from "@/lib/api/file"; @@ -99,6 +101,7 @@ export const InteractiveDataTable: React.FC = ({ onRefresh, }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 + const { user } = useAuth(); // 사용자 정보 가져오기 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [searchValues, setSearchValues] = useState>({}); @@ -134,6 +137,13 @@ export const InteractiveDataTable: React.FC = ({ // 공통코드 관리 상태 const [codeOptions, setCodeOptions] = useState>>({}); + // 🆕 검색 필터 관련 상태 (FlowWidget과 동일) + const [searchFilterColumns, setSearchFilterColumns] = useState>(new Set()); // 검색 필터로 사용할 컬럼 + const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그 + const [allAvailableColumns, setAllAvailableColumns] = useState([]); // 전체 컬럼 목록 + const [filteredData, setFilteredData] = useState([]); // 필터링된 데이터 + const [columnLabels, setColumnLabels] = useState>({}); // 컬럼명 -> 라벨 매핑 + // 공통코드 옵션 가져오기 const loadCodeOptions = useCallback( async (categoryCode: string) => { @@ -633,6 +643,31 @@ export const InteractiveDataTable: React.FC = ({ try { const columns = await tableTypeApi.getColumns(component.tableName); setTableColumns(columns); + + // 🆕 전체 컬럼 목록 설정 + const columnNames = columns.map(col => col.columnName); + setAllAvailableColumns(columnNames); + + // 🆕 컬럼명 -> 라벨 매핑 생성 + const labels: Record = {}; + columns.forEach(col => { + labels[col.columnName] = col.displayName || col.columnName; + }); + setColumnLabels(labels); + + // 🆕 localStorage에서 필터 설정 복원 + if (user?.userId && component.componentId) { + const storageKey = `table-search-filter-${user.userId}-${component.componentId}`; + const savedFilter = localStorage.getItem(storageKey); + if (savedFilter) { + try { + const parsed = JSON.parse(savedFilter); + setSearchFilterColumns(new Set(parsed)); + } catch (e) { + console.error("필터 설정 복원 실패:", e); + } + } + } } catch (error) { // console.error("테이블 컬럼 정보 로드 실패:", error); } @@ -641,7 +676,7 @@ export const InteractiveDataTable: React.FC = ({ if (component.tableName) { fetchTableColumns(); } - }, [component.tableName]); + }, [component.tableName, component.componentId, user?.userId]); // 실제 사용할 필터 (설정된 필터만 사용, 자동 생성 안함) const searchFilters = useMemo(() => { @@ -1052,6 +1087,29 @@ export const InteractiveDataTable: React.FC = ({ } }, [isAdding]); + // 🆕 검색 필터 저장 함수 + const handleSaveSearchFilter = useCallback(() => { + if (user?.userId && component.componentId) { + const storageKey = `table-search-filter-${user.userId}-${component.componentId}`; + const filterArray = Array.from(searchFilterColumns); + localStorage.setItem(storageKey, JSON.stringify(filterArray)); + toast.success("검색 필터 설정이 저장되었습니다."); + } + }, [user?.userId, component.componentId, searchFilterColumns]); + + // 🆕 검색 필터 토글 함수 + const handleToggleFilterColumn = useCallback((columnName: string) => { + setSearchFilterColumns((prev) => { + const newSet = new Set(prev); + if (newSet.has(columnName)) { + newSet.delete(columnName); + } else { + newSet.add(columnName); + } + return newSet; + }); + }, []); + // 데이터 삭제 핸들러 const handleDeleteData = useCallback(() => { if (selectedRows.size === 0) { diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 7249eb6c..b5b0700d 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -253,6 +253,12 @@ export const TableListComponent: React.FC = ({ // 필터 설정 관련 상태 const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); const [visibleFilterColumns, setVisibleFilterColumns] = useState>(new Set()); + + // 필터 설정 키 생성 + const filterSettingKey = useMemo(() => { + if (!tableConfig.selectedTable) return null; + return `table-list-filter-${tableConfig.selectedTable}`; + }, [tableConfig.selectedTable]); const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, { enableBatchLoading: true, @@ -716,7 +722,7 @@ export const TableListComponent: React.FC = ({ // 저장된 필터 설정 불러오기 useEffect(() => { - if (!filterSettingKey) return; + if (!filterSettingKey || visibleColumns.length === 0) return; try { const saved = localStorage.getItem(filterSettingKey); @@ -724,17 +730,14 @@ export const TableListComponent: React.FC = ({ const savedFilters = JSON.parse(saved); setVisibleFilterColumns(new Set(savedFilters)); } else { - // 초기값: 모든 필터 표시 - const allFilters = (tableConfig.filter?.filters || []).map((f) => f.columnName); - setVisibleFilterColumns(new Set(allFilters)); + // 초기값: 빈 Set (아무것도 선택 안 함) + setVisibleFilterColumns(new Set()); } } catch (error) { console.error("필터 설정 불러오기 실패:", error); - // 기본값으로 모든 필터 표시 - const allFilters = (tableConfig.filter?.filters || []).map((f) => f.columnName); - setVisibleFilterColumns(new Set(allFilters)); + setVisibleFilterColumns(new Set()); } - }, [filterSettingKey, tableConfig.filter?.filters]); + }, [filterSettingKey, visibleColumns]); // 필터 설정 저장 const saveFilterSettings = useCallback(() => { @@ -743,12 +746,17 @@ export const TableListComponent: React.FC = ({ try { localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(visibleFilterColumns))); setIsFilterSettingOpen(false); + toast.success("검색 필터 설정이 저장되었습니다"); + + // 검색 값 초기화 + setSearchValues({}); } catch (error) { console.error("필터 설정 저장 실패:", error); + toast.error("설정 저장에 실패했습니다"); } }, [filterSettingKey, visibleFilterColumns]); - // 필터 토글 + // 필터 컬럼 토글 const toggleFilterVisibility = useCallback((columnName: string) => { setVisibleFilterColumns((prev) => { const newSet = new Set(prev); @@ -761,10 +769,30 @@ export const TableListComponent: React.FC = ({ }); }, []); - // 표시할 필터 목록 + // 전체 선택/해제 + const toggleAllFilters = useCallback(() => { + const filterableColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); + const columnNames = filterableColumns.map((col) => col.columnName); + + if (visibleFilterColumns.size === columnNames.length) { + // 전체 해제 + setVisibleFilterColumns(new Set()); + } else { + // 전체 선택 + setVisibleFilterColumns(new Set(columnNames)); + } + }, [visibleFilterColumns, visibleColumns]); + + // 표시할 필터 목록 (선택된 컬럼만) const activeFilters = useMemo(() => { - return (tableConfig.filter?.filters || []).filter((f) => visibleFilterColumns.has(f.columnName)); - }, [tableConfig.filter?.filters, visibleFilterColumns]); + return visibleColumns + .filter((col) => col.columnName !== "__checkbox__" && visibleFilterColumns.has(col.columnName)) + .map((col) => ({ + columnName: col.columnName, + label: columnLabels[col.columnName] || col.displayName || col.columnName, + type: col.format || "text", + })); + }, [visibleColumns, visibleFilterColumns, columnLabels]); useEffect(() => { fetchColumnLabels(); @@ -1244,29 +1272,63 @@ export const TableListComponent: React.FC = ({ 검색 필터 설정 - 표시할 검색 필터를 선택하세요. 선택하지 않은 필터는 숨겨집니다. + 검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다. -
- {(tableConfig.filter?.filters || []).map((filter) => ( -
- toggleFilterVisibility(filter.columnName)} - /> - -
- ))} +
+ {/* 전체 선택/해제 */} +
+ col.columnName !== "__checkbox__").length && + visibleColumns.filter((col) => col.columnName !== "__checkbox__").length > 0 + } + onCheckedChange={toggleAllFilters} + /> + + + {visibleFilterColumns.size} / {visibleColumns.filter((col) => col.columnName !== "__checkbox__").length} + 개 + +
+ + {/* 컬럼 목록 */} +
+ {visibleColumns + .filter((col) => col.columnName !== "__checkbox__") + .map((col) => ( +
+ toggleFilterVisibility(col.columnName)} + /> + +
+ ))} +
+ + {/* 선택된 컬럼 개수 안내 */} +
+ {visibleFilterColumns.size === 0 ? ( + 검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요 + ) : ( + + 총 {visibleFilterColumns.size}개의 검색 필터가 + 표시됩니다 + + )} +