feat: TableListComponent에 FlowWidget과 동일한 필터 설정 UI 구현

- 전체 선택/해제 기능 추가
- 선택된 컬럼 개수 표시 추가
- 필터 설정 localStorage 저장/로드 기능
- 체크된 항목만 실제 검색 필터로 표시
- 저장 시 Toast 알림 추가
- FlowWidget과 완전히 동일한 UI/UX 적용
This commit is contained in:
kjs
2025-11-03 13:59:12 +09:00
parent e0e7bc387e
commit 297870a24c
2 changed files with 153 additions and 33 deletions

View File

@@ -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<InteractiveDataTableProps> = ({
onRefresh,
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { user } = useAuth(); // 사용자 정보 가져오기
const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false);
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
@@ -134,6 +137,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
// 공통코드 관리 상태
const [codeOptions, setCodeOptions] = useState<Record<string, Array<{ value: string; label: string }>>>({});
// 🆕 검색 필터 관련 상태 (FlowWidget과 동일)
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
// 공통코드 옵션 가져오기
const loadCodeOptions = useCallback(
async (categoryCode: string) => {
@@ -633,6 +643,31 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
try {
const columns = await tableTypeApi.getColumns(component.tableName);
setTableColumns(columns);
// 🆕 전체 컬럼 목록 설정
const columnNames = columns.map(col => col.columnName);
setAllAvailableColumns(columnNames);
// 🆕 컬럼명 -> 라벨 매핑 생성
const labels: Record<string, string> = {};
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<InteractiveDataTableProps> = ({
if (component.tableName) {
fetchTableColumns();
}
}, [component.tableName]);
}, [component.tableName, component.componentId, user?.userId]);
// 실제 사용할 필터 (설정된 필터만 사용, 자동 생성 안함)
const searchFilters = useMemo(() => {
@@ -1052,6 +1087,29 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
}
}, [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) {

View File

@@ -253,6 +253,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 필터 설정 관련 상태
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false);
const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(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<TableListComponentProps> = ({
// 저장된 필터 설정 불러오기
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
});
}, []);
// 표시할 필터 목록
// 전체 선택/해제
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<TableListComponentProps> = ({
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
. .
. .
</DialogDescription>
</DialogHeader>
<div className="max-h-[60vh] space-y-2 overflow-y-auto py-4">
{(tableConfig.filter?.filters || []).map((filter) => (
<div
key={filter.columnName}
className="flex items-center space-x-3 rounded-lg p-3 hover:bg-gray-50"
>
<Checkbox
id={`filter-${filter.columnName}`}
checked={visibleFilterColumns.has(filter.columnName)}
onCheckedChange={() => toggleFilterVisibility(filter.columnName)}
/>
<label
htmlFor={`filter-${filter.columnName}`}
className="flex-1 cursor-pointer text-sm"
>
{columnLabels[filter.columnName] || filter.label || filter.columnName}
</label>
</div>
))}
<div className="space-y-3 sm:space-y-4">
{/* 전체 선택/해제 */}
<div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
<Checkbox
id="select-all-filters"
checked={
visibleFilterColumns.size ===
visibleColumns.filter((col) => col.columnName !== "__checkbox__").length &&
visibleColumns.filter((col) => col.columnName !== "__checkbox__").length > 0
}
onCheckedChange={toggleAllFilters}
/>
<Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
/
</Label>
<span className="text-muted-foreground text-xs">
{visibleFilterColumns.size} / {visibleColumns.filter((col) => col.columnName !== "__checkbox__").length}
</span>
</div>
{/* 컬럼 목록 */}
<div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
{visibleColumns
.filter((col) => col.columnName !== "__checkbox__")
.map((col) => (
<div key={col.columnName} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
<Checkbox
id={`filter-${col.columnName}`}
checked={visibleFilterColumns.has(col.columnName)}
onCheckedChange={() => toggleFilterVisibility(col.columnName)}
/>
<Label
htmlFor={`filter-${col.columnName}`}
className="flex-1 cursor-pointer text-xs font-normal sm:text-sm"
>
{columnLabels[col.columnName] || col.displayName || col.columnName}
</Label>
</div>
))}
</div>
{/* 선택된 컬럼 개수 안내 */}
<div className="text-muted-foreground bg-muted/30 rounded p-3 text-center text-xs">
{visibleFilterColumns.size === 0 ? (
<span> 1 </span>
) : (
<span>
<span className="text-primary font-semibold">{visibleFilterColumns.size}</span>
</span>
)}
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">