feat: TableListComponent에 FlowWidget과 동일한 필터 설정 UI 구현
- 전체 선택/해제 기능 추가 - 선택된 컬럼 개수 표시 추가 - 필터 설정 localStorage 저장/로드 기능 - 체크된 항목만 실제 검색 필터로 표시 - 저장 시 Toast 알림 추가 - FlowWidget과 완전히 동일한 UI/UX 적용
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user