feat: DataTableTemplate에 플로우 위젯 스타일 검색 필터 기능 추가

 새로운 기능
- 플로우 위젯과 동일한 검색 필터 설정 기능 구현
- 사용자가 원하는 컬럼만 선택하여 검색 가능
- localStorage 기반 필터 설정 저장/복원

🎨 UI 추가
- '검색 필터 설정' 버튼 (FlowWidget 스타일)
- 선택된 컬럼의 동적 검색 입력 필드
- 필터 개수 뱃지 표시
- 체크박스 기반 필터 설정 다이얼로그

🔧 기술적 구현
- searchFilterColumns 상태로 선택된 컬럼 관리
- searchValues 상태로 각 컬럼별 검색값 관리
- useAuth 훅으로 사용자별 필터 설정 저장
- Grid 레이아웃으로 검색 필드 반응형 배치

📝 변경된 파일
- frontend/components/screen/templates/DataTableTemplate.tsx

 테스트 완료
- 필터 설정 저장/복원
- 동적 검색 필드 생성
- 반응형 레이아웃
- 미리보기 모드에서 비활성화
This commit is contained in:
kjs 2025-11-03 13:51:08 +09:00
parent 714511c3cf
commit cbf8576897
1 changed files with 147 additions and 7 deletions

View File

@ -1,6 +1,6 @@
"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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@ -8,6 +8,9 @@ import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
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 = "",
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(() => {
return columns || [];
@ -138,6 +148,54 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
}, [isPreview]);
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 (
<Card className={`h-full w-full ${className}`} style={style}>
@ -178,23 +236,65 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
</CardHeader>
<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 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 && (
<Button variant="outline" disabled={isPreview}>
<Search className="mr-2 h-4 w-4" />
{actions.searchButtonText}
</Button>
)}
</div>
{/* 필터 영역 */}
{/* 기존 필터 영역 (이제는 사용하지 않음) */}
{filters.length > 0 && (
<div className="flex items-center space-x-2">
<Filter className="text-muted-foreground h-4 w-4" />
@ -352,6 +452,46 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
</div>
)}
</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>
);
};