diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
| commit | e9897d416b3e7327bbd4d4aef887eee37751ae82 (patch) | |
| tree | bd20ce6eadf9b21755bd7425492d2d31c7700a0e /components/mail/mail-templates-client.tsx | |
| parent | 3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff) | |
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'components/mail/mail-templates-client.tsx')
| -rw-r--r-- | components/mail/mail-templates-client.tsx | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/components/mail/mail-templates-client.tsx b/components/mail/mail-templates-client.tsx new file mode 100644 index 00000000..7c4dafdf --- /dev/null +++ b/components/mail/mail-templates-client.tsx @@ -0,0 +1,218 @@ +'use client';
+
+import { useState, useEffect, useTransition } from 'react';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Search, Edit, FileText, ChevronUp, ChevronDown } from 'lucide-react';
+import { toast } from 'sonner';
+import { getTemplatesAction, TemplateFile } from '@/lib/mail/service';
+
+type Template = TemplateFile;
+
+interface MailTemplatesClientProps {
+ initialData?: Template[];
+}
+
+type SortField = 'name' | 'lastModified';
+type SortDirection = 'asc' | 'desc';
+
+export default function MailTemplatesClient({ initialData = [] }: MailTemplatesClientProps) {
+ const params = useParams();
+ const lng = (params?.lng as string) || 'ko';
+
+ const [templates, setTemplates] = useState<Template[]>(initialData);
+ const [loading, setLoading] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortField, setSortField] = useState<SortField>('name');
+ const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
+ const [, startTransition] = useTransition();
+
+ // 템플릿 목록 조회
+ const fetchTemplates = async () => {
+ try {
+ setLoading(true);
+ const search = searchQuery || undefined;
+
+ startTransition(async () => {
+ const result = await getTemplatesAction(search);
+
+ if (result.success && result.data) {
+ setTemplates(result.data);
+ } else {
+ toast.error(result.error || '템플릿 목록을 가져오는데 실패했습니다.');
+ }
+ setLoading(false);
+ });
+ } catch (error) {
+ console.error('Error fetching templates:', error);
+ toast.error('템플릿 목록을 가져오는데 실패했습니다.');
+ setLoading(false);
+ }
+ };
+
+ // 검색 핸들러
+ const handleSearch = () => {
+ fetchTemplates();
+ };
+
+ // 정렬 함수
+ const sortTemplates = (templates: Template[]) => {
+ return [...templates].sort((a, b) => {
+ let aValue: string | Date;
+ let bValue: string | Date;
+
+ if (sortField === 'name') {
+ aValue = a.name;
+ bValue = b.name;
+ } else {
+ aValue = new Date(a.lastModified);
+ bValue = new Date(b.lastModified);
+ }
+
+ if (aValue < bValue) {
+ return sortDirection === 'asc' ? -1 : 1;
+ }
+ if (aValue > bValue) {
+ return sortDirection === 'asc' ? 1 : -1;
+ }
+ return 0;
+ });
+ };
+
+ // 정렬 핸들러
+ const handleSort = (field: SortField) => {
+ if (sortField === field) {
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
+ } else {
+ setSortField(field);
+ setSortDirection('asc');
+ }
+ };
+
+ // 정렬된 템플릿 목록
+ const sortedTemplates = sortTemplates(templates);
+
+ useEffect(() => {
+ if (searchQuery !== '') {
+ fetchTemplates();
+ } else {
+ setTemplates(initialData);
+ }
+ }, [searchQuery, initialData]);
+
+ return (
+ <div className="space-y-6">
+ {/* 검색 */}
+ <div className="flex items-center gap-4">
+ <div className="relative flex-1 max-w-md">
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
+ <Input
+ placeholder="템플릿 이름 또는 내용으로 검색..."
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ className="pl-10"
+ onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
+ />
+ </div>
+ <Button onClick={handleSearch} variant="outline">
+ 검색
+ </Button>
+ <Button
+ variant="outline"
+ onClick={() => window.location.reload()}
+ >
+ 새로고침
+ </Button>
+ </div>
+
+ {/* 템플릿 테이블 */}
+ <div className="bg-white rounded-lg shadow">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>
+ <button
+ className="flex items-center gap-1 hover:text-foreground"
+ onClick={() => handleSort('name')}
+ >
+ 이름
+ {sortField === 'name' && (
+ sortDirection === 'asc' ? (
+ <ChevronUp className="h-4 w-4" />
+ ) : (
+ <ChevronDown className="h-4 w-4" />
+ )
+ )}
+ </button>
+ </TableHead>
+ <TableHead>
+ <button
+ className="flex items-center gap-1 hover:text-foreground"
+ onClick={() => handleSort('lastModified')}
+ >
+ 수정일
+ {sortField === 'lastModified' && (
+ sortDirection === 'asc' ? (
+ <ChevronUp className="h-4 w-4" />
+ ) : (
+ <ChevronDown className="h-4 w-4" />
+ )
+ )}
+ </button>
+ </TableHead>
+ <TableHead className="text-right">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {loading ? (
+ <TableRow>
+ <TableCell colSpan={3} className="text-center py-8">
+ 로딩 중...
+ </TableCell>
+ </TableRow>
+ ) : templates.length === 0 ? (
+ <TableRow>
+ <TableCell colSpan={3} className="text-center py-8 text-gray-500">
+ 템플릿이 없습니다.
+ </TableCell>
+ </TableRow>
+ ) : (
+ sortedTemplates.map((template) => (
+ <TableRow key={template.name}>
+ <TableCell className="font-medium">
+ <div className="flex items-center gap-2">
+ <FileText className="h-4 w-4" />
+ {template.name}
+ </div>
+ </TableCell>
+ <TableCell>
+ {new Date(template.lastModified).toLocaleString('ko-KR')}
+ </TableCell>
+ <TableCell className="text-right">
+ <div className="flex justify-end gap-2">
+ <Link href={`/${lng}/evcp/email-template/${template.name}`}>
+ <Button variant="outline" size="sm">
+ <Edit className="h-4 w-4" />
+ </Button>
+ </Link>
+ </div>
+ </TableCell>
+ </TableRow>
+ ))
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ </div>
+ );
+}
\ No newline at end of file |
