255 lines
9.3 KiB
TypeScript
255 lines
9.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from 'react';
|
|
import {
|
|
Mail,
|
|
Edit2,
|
|
Trash2,
|
|
Power,
|
|
PowerOff,
|
|
Search,
|
|
Calendar,
|
|
Zap,
|
|
CheckCircle,
|
|
XCircle,
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { MailAccount } from '@/lib/api/mail';
|
|
|
|
interface MailAccountTableProps {
|
|
accounts: MailAccount[];
|
|
onEdit: (account: MailAccount) => void;
|
|
onDelete: (account: MailAccount) => void;
|
|
onToggleStatus: (account: MailAccount) => void;
|
|
}
|
|
|
|
export default function MailAccountTable({
|
|
accounts,
|
|
onEdit,
|
|
onDelete,
|
|
onToggleStatus,
|
|
}: MailAccountTableProps) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [sortField, setSortField] = useState<keyof MailAccount>('createdAt');
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
|
|
|
// 검색 필터링
|
|
const filteredAccounts = accounts.filter(
|
|
(account) =>
|
|
account.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
account.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
account.smtpHost.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
// 정렬
|
|
const sortedAccounts = [...filteredAccounts].sort((a, b) => {
|
|
const aValue = a[sortField];
|
|
const bValue = b[sortField];
|
|
|
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
return sortOrder === 'asc'
|
|
? aValue.localeCompare(bValue)
|
|
: bValue.localeCompare(aValue);
|
|
}
|
|
|
|
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
return sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
|
|
const handleSort = (field: keyof MailAccount) => {
|
|
if (sortField === field) {
|
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
setSortField(field);
|
|
setSortOrder('asc');
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
if (accounts.length === 0) {
|
|
return (
|
|
<div className="bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-12 text-center border-2 border-dashed border-gray-300">
|
|
<Mail className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
|
<p className="text-lg font-medium text-gray-600 mb-2">
|
|
등록된 메일 계정이 없습니다
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
"새 계정 추가" 버튼을 클릭하여 첫 번째 메일 계정을 등록하세요.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 검색 */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder="계정명, 이메일, 서버로 검색..."
|
|
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gradient-to-r from-slate-50 to-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th
|
|
className="px-6 py-4 text-left text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition"
|
|
onClick={() => handleSort('name')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Mail className="w-4 h-4 text-orange-500" />
|
|
계정명
|
|
{sortField === 'name' && (
|
|
<span className="text-xs">
|
|
{sortOrder === 'asc' ? '↑' : '↓'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-4 text-left text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition"
|
|
onClick={() => handleSort('email')}
|
|
>
|
|
이메일 주소
|
|
</th>
|
|
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700">
|
|
SMTP 서버
|
|
</th>
|
|
<th
|
|
className="px-6 py-4 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition"
|
|
onClick={() => handleSort('status')}
|
|
>
|
|
상태
|
|
</th>
|
|
<th
|
|
className="px-6 py-4 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition"
|
|
onClick={() => handleSort('dailyLimit')}
|
|
>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Zap className="w-4 h-4 text-orange-500" />
|
|
일일 제한
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-4 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition"
|
|
onClick={() => handleSort('createdAt')}
|
|
>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Calendar className="w-4 h-4 text-orange-500" />
|
|
생성일
|
|
</div>
|
|
</th>
|
|
<th className="px-6 py-4 text-center text-sm font-semibold text-gray-700">
|
|
액션
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{sortedAccounts.map((account) => (
|
|
<tr
|
|
key={account.id}
|
|
className="hover:bg-orange-50/50 transition-colors"
|
|
>
|
|
<td className="px-6 py-4">
|
|
<div className="font-medium text-gray-900">{account.name}</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-sm text-gray-600">{account.email}</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-sm text-gray-600">
|
|
{account.smtpHost}:{account.smtpPort}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
{account.smtpSecure ? 'SSL' : 'TLS'}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<button
|
|
onClick={() => onToggleStatus(account)}
|
|
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all hover:scale-105 ${
|
|
account.status === 'active'
|
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
{account.status === 'active' ? (
|
|
<>
|
|
<CheckCircle className="w-3.5 h-3.5" />
|
|
활성
|
|
</>
|
|
) : (
|
|
<>
|
|
<XCircle className="w-3.5 h-3.5" />
|
|
비활성
|
|
</>
|
|
)}
|
|
</button>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{account.dailyLimit > 0
|
|
? account.dailyLimit.toLocaleString()
|
|
: '무제한'}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<div className="text-sm text-gray-600">
|
|
{formatDate(account.createdAt)}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<button
|
|
onClick={() => onEdit(account)}
|
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
title="수정"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => onDelete(account)}
|
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 결과 요약 */}
|
|
<div className="text-sm text-gray-600 text-center">
|
|
전체 {accounts.length}개 중 {sortedAccounts.length}개 표시
|
|
{searchTerm && ` (검색: "${searchTerm}")`}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|