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, Folder,
FolderOpen, FolderOpen,
Grid, Grid,
Filter,
} from "lucide-react"; } from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen"; import { tableTypeApi } from "@/lib/api/screen";
import { commonCodeApi } from "@/lib/api/commonCode"; import { commonCodeApi } from "@/lib/api/commonCode";
import { getCurrentUser, UserInfo } from "@/lib/api/client"; import { getCurrentUser, UserInfo } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { downloadFile, getLinkedFiles, getFilePreviewUrl, getDirectFileUrl } from "@/lib/api/file"; import { downloadFile, getLinkedFiles, getFilePreviewUrl, getDirectFileUrl } from "@/lib/api/file";
@ -99,6 +101,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
onRefresh, onRefresh,
}) => { }) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { user } = useAuth(); // 사용자 정보 가져오기
const [data, setData] = useState<Record<string, any>[]>([]); const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchValues, setSearchValues] = useState<Record<string, any>>({}); 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 }>>>({}); 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( const loadCodeOptions = useCallback(
async (categoryCode: string) => { async (categoryCode: string) => {
@ -633,6 +643,31 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
try { try {
const columns = await tableTypeApi.getColumns(component.tableName); const columns = await tableTypeApi.getColumns(component.tableName);
setTableColumns(columns); 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) { } catch (error) {
// console.error("테이블 컬럼 정보 로드 실패:", error); // console.error("테이블 컬럼 정보 로드 실패:", error);
} }
@ -641,7 +676,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
if (component.tableName) { if (component.tableName) {
fetchTableColumns(); fetchTableColumns();
} }
}, [component.tableName]); }, [component.tableName, component.componentId, user?.userId]);
// 실제 사용할 필터 (설정된 필터만 사용, 자동 생성 안함) // 실제 사용할 필터 (설정된 필터만 사용, 자동 생성 안함)
const searchFilters = useMemo(() => { const searchFilters = useMemo(() => {
@ -1052,6 +1087,29 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
} }
}, [isAdding]); }, [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(() => { const handleDeleteData = useCallback(() => {
if (selectedRows.size === 0) { if (selectedRows.size === 0) {

View File

@ -254,6 +254,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false);
const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(new Set()); 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, { const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, {
enableBatchLoading: true, enableBatchLoading: true,
preloadCommonCodes: true, preloadCommonCodes: true,
@ -716,7 +722,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 저장된 필터 설정 불러오기 // 저장된 필터 설정 불러오기
useEffect(() => { useEffect(() => {
if (!filterSettingKey) return; if (!filterSettingKey || visibleColumns.length === 0) return;
try { try {
const saved = localStorage.getItem(filterSettingKey); const saved = localStorage.getItem(filterSettingKey);
@ -724,17 +730,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const savedFilters = JSON.parse(saved); const savedFilters = JSON.parse(saved);
setVisibleFilterColumns(new Set(savedFilters)); setVisibleFilterColumns(new Set(savedFilters));
} else { } else {
// 초기값: 모든 필터 표시 // 초기값: 빈 Set (아무것도 선택 안 함)
const allFilters = (tableConfig.filter?.filters || []).map((f) => f.columnName); setVisibleFilterColumns(new Set());
setVisibleFilterColumns(new Set(allFilters));
} }
} catch (error) { } catch (error) {
console.error("필터 설정 불러오기 실패:", error); console.error("필터 설정 불러오기 실패:", error);
// 기본값으로 모든 필터 표시 setVisibleFilterColumns(new Set());
const allFilters = (tableConfig.filter?.filters || []).map((f) => f.columnName);
setVisibleFilterColumns(new Set(allFilters));
} }
}, [filterSettingKey, tableConfig.filter?.filters]); }, [filterSettingKey, visibleColumns]);
// 필터 설정 저장 // 필터 설정 저장
const saveFilterSettings = useCallback(() => { const saveFilterSettings = useCallback(() => {
@ -743,12 +746,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
try { try {
localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(visibleFilterColumns))); localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(visibleFilterColumns)));
setIsFilterSettingOpen(false); setIsFilterSettingOpen(false);
toast.success("검색 필터 설정이 저장되었습니다");
// 검색 값 초기화
setSearchValues({});
} catch (error) { } catch (error) {
console.error("필터 설정 저장 실패:", error); console.error("필터 설정 저장 실패:", error);
toast.error("설정 저장에 실패했습니다");
} }
}, [filterSettingKey, visibleFilterColumns]); }, [filterSettingKey, visibleFilterColumns]);
// 필터 토글 // 필터 컬럼 토글
const toggleFilterVisibility = useCallback((columnName: string) => { const toggleFilterVisibility = useCallback((columnName: string) => {
setVisibleFilterColumns((prev) => { setVisibleFilterColumns((prev) => {
const newSet = new Set(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(() => { const activeFilters = useMemo(() => {
return (tableConfig.filter?.filters || []).filter((f) => visibleFilterColumns.has(f.columnName)); return visibleColumns
}, [tableConfig.filter?.filters, visibleFilterColumns]); .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(() => { useEffect(() => {
fetchColumnLabels(); fetchColumnLabels();
@ -1244,31 +1272,65 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<DialogHeader> <DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle> <DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm"> <DialogDescription className="text-xs sm:text-sm">
. . . .
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="max-h-[60vh] space-y-2 overflow-y-auto py-4"> <div className="space-y-3 sm:space-y-4">
{(tableConfig.filter?.filters || []).map((filter) => ( {/* 전체 선택/해제 */}
<div <div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
key={filter.columnName}
className="flex items-center space-x-3 rounded-lg p-3 hover:bg-gray-50"
>
<Checkbox <Checkbox
id={`filter-${filter.columnName}`} id="select-all-filters"
checked={visibleFilterColumns.has(filter.columnName)} checked={
onCheckedChange={() => toggleFilterVisibility(filter.columnName)} visibleFilterColumns.size ===
visibleColumns.filter((col) => col.columnName !== "__checkbox__").length &&
visibleColumns.filter((col) => col.columnName !== "__checkbox__").length > 0
}
onCheckedChange={toggleAllFilters}
/> />
<label <Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
htmlFor={`filter-${filter.columnName}`} /
className="flex-1 cursor-pointer 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[filter.columnName] || filter.label || filter.columnName} {columnLabels[col.columnName] || col.displayName || col.columnName}
</label> </Label>
</div> </div>
))} ))}
</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"> <DialogFooter className="gap-2 sm:gap-0">
<Button <Button
variant="outline" variant="outline"