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