feat: DataTableTemplate에 플로우 위젯 스타일 검색 필터 기능 추가
✨ 새로운 기능 - 플로우 위젯과 동일한 검색 필터 설정 기능 구현 - 사용자가 원하는 컬럼만 선택하여 검색 가능 - localStorage 기반 필터 설정 저장/복원 🎨 UI 추가 - '검색 필터 설정' 버튼 (FlowWidget 스타일) - 선택된 컬럼의 동적 검색 입력 필드 - 필터 개수 뱃지 표시 - 체크박스 기반 필터 설정 다이얼로그 🔧 기술적 구현 - searchFilterColumns 상태로 선택된 컬럼 관리 - searchValues 상태로 각 컬럼별 검색값 관리 - useAuth 훅으로 사용자별 필터 설정 저장 - Grid 레이아웃으로 검색 필드 반응형 배치 📝 변경된 파일 - frontend/components/screen/templates/DataTableTemplate.tsx ✅ 테스트 완료 - 필터 설정 저장/복원 - 동적 검색 필드 생성 - 반응형 레이아웃 - 미리보기 모드에서 비활성화
This commit is contained in:
parent
714511c3cf
commit
cbf8576897
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Table, Filter, Search, Download, RefreshCw, Plus, Edit, Trash2 } from "lucide-react";
|
import { Table, Filter, Search, Download, RefreshCw, Plus, Edit, Trash2 } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -8,6 +8,9 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 데이터 테이블 템플릿 컴포넌트
|
* 데이터 테이블 템플릿 컴포넌트
|
||||||
|
|
@ -121,6 +124,13 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
|
||||||
className = "",
|
className = "",
|
||||||
isPreview = true,
|
isPreview = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// 🆕 검색 필터 관련 상태
|
||||||
|
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set());
|
||||||
|
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false);
|
||||||
|
const [searchValues, setSearchValues] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 설정된 컬럼만 사용 (자동 생성 안함)
|
// 설정된 컬럼만 사용 (자동 생성 안함)
|
||||||
const defaultColumns = React.useMemo(() => {
|
const defaultColumns = React.useMemo(() => {
|
||||||
return columns || [];
|
return columns || [];
|
||||||
|
|
@ -139,6 +149,54 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
|
||||||
|
|
||||||
const visibleColumns = defaultColumns.filter((col) => col.visible);
|
const visibleColumns = defaultColumns.filter((col) => col.visible);
|
||||||
|
|
||||||
|
// 🆕 컬럼명 -> 라벨 매핑
|
||||||
|
const columnLabels = React.useMemo(() => {
|
||||||
|
const labels: Record<string, string> = {};
|
||||||
|
defaultColumns.forEach(col => {
|
||||||
|
labels[col.id] = col.label;
|
||||||
|
});
|
||||||
|
return labels;
|
||||||
|
}, [defaultColumns]);
|
||||||
|
|
||||||
|
// 🆕 localStorage에서 필터 설정 복원
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.userId && title) {
|
||||||
|
const storageKey = `datatable-search-filter-${user.userId}-${title}`;
|
||||||
|
const savedFilter = localStorage.getItem(storageKey);
|
||||||
|
if (savedFilter) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedFilter);
|
||||||
|
setSearchFilterColumns(new Set(parsed));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("필터 설정 복원 실패:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [user?.userId, title]);
|
||||||
|
|
||||||
|
// 🆕 필터 저장 함수
|
||||||
|
const handleSaveSearchFilter = useCallback(() => {
|
||||||
|
if (user?.userId && title) {
|
||||||
|
const storageKey = `datatable-search-filter-${user.userId}-${title}`;
|
||||||
|
const filterArray = Array.from(searchFilterColumns);
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(filterArray));
|
||||||
|
toast.success("검색 필터 설정이 저장되었습니다.");
|
||||||
|
}
|
||||||
|
}, [user?.userId, title, searchFilterColumns]);
|
||||||
|
|
||||||
|
// 🆕 필터 토글 함수
|
||||||
|
const handleToggleFilterColumn = useCallback((columnId: string) => {
|
||||||
|
setSearchFilterColumns((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(columnId)) {
|
||||||
|
newSet.delete(columnId);
|
||||||
|
} else {
|
||||||
|
newSet.add(columnId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`h-full w-full ${className}`} style={style}>
|
<Card className={`h-full w-full ${className}`} style={style}>
|
||||||
{/* 헤더 영역 */}
|
{/* 헤더 영역 */}
|
||||||
|
|
@ -178,23 +236,65 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
{/* 🆕 검색 필터 설정 버튼 영역 */}
|
||||||
|
{defaultColumns.length > 0 && (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsFilterSettingOpen(true)}
|
||||||
|
disabled={isPreview}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
검색 필터 설정
|
||||||
|
{searchFilterColumns.size > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-1 h-5 px-1.5 text-[10px]">
|
||||||
|
{searchFilterColumns.size}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🆕 선택된 컬럼의 검색 입력 필드 */}
|
||||||
|
{searchFilterColumns.size > 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-3 rounded-lg border bg-gray-50/50 p-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from(searchFilterColumns).map((columnId) => {
|
||||||
|
const column = defaultColumns.find(col => col.id === columnId);
|
||||||
|
if (!column) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={columnId} className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium text-gray-700">
|
||||||
|
{column.label}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
placeholder={`${column.label} 검색...`}
|
||||||
|
value={searchValues[columnId] || ""}
|
||||||
|
onChange={(e) => setSearchValues(prev => ({...prev, [columnId]: e.target.value}))}
|
||||||
|
disabled={isPreview}
|
||||||
|
className="h-9 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 검색 및 필터 영역 */}
|
{/* 검색 및 필터 영역 */}
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
|
||||||
{/* 검색 입력 */}
|
{/* 검색 입력 */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative min-w-[200px] flex-1">
|
|
||||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
||||||
<Input placeholder="검색어를 입력하세요..." className="pl-10" disabled={isPreview} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{actions.showSearchButton && (
|
{actions.showSearchButton && (
|
||||||
<Button variant="outline" disabled={isPreview}>
|
<Button variant="outline" disabled={isPreview}>
|
||||||
|
<Search className="mr-2 h-4 w-4" />
|
||||||
{actions.searchButtonText}
|
{actions.searchButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필터 영역 */}
|
{/* 기존 필터 영역 (이제는 사용하지 않음) */}
|
||||||
{filters.length > 0 && (
|
{filters.length > 0 && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Filter className="text-muted-foreground h-4 w-4" />
|
<Filter className="text-muted-foreground h-4 w-4" />
|
||||||
|
|
@ -352,6 +452,46 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* 🆕 검색 필터 설정 다이얼로그 */}
|
||||||
|
<Dialog open={isFilterSettingOpen} onOpenChange={setIsFilterSettingOpen}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>검색 필터 설정</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
표시할 검색 필터를 선택하세요. 선택하지 않은 필터는 숨겨집니다.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="max-h-[400px] space-y-2 overflow-y-auto py-4">
|
||||||
|
{defaultColumns.map((column) => (
|
||||||
|
<div key={column.id} className="flex items-center space-x-3 rounded-lg p-3 hover:bg-gray-50">
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-${column.id}`}
|
||||||
|
checked={searchFilterColumns.has(column.id)}
|
||||||
|
onCheckedChange={() => handleToggleFilterColumn(column.id)}
|
||||||
|
/>
|
||||||
|
<label htmlFor={`filter-${column.id}`} className="flex-1 cursor-pointer text-sm">
|
||||||
|
{column.label}
|
||||||
|
<span className="ml-2 text-xs text-gray-500">({column.type})</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button variant="outline" onClick={() => setIsFilterSettingOpen(false)}>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
handleSaveSearchFilter();
|
||||||
|
setIsFilterSettingOpen(false);
|
||||||
|
}}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue