229 lines
9.3 KiB
TypeScript
229 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;
|
|
onTestConnection: (account: MailAccount) => void;
|
|
}
|
|
|
|
export default function MailAccountTable({
|
|
accounts,
|
|
onEdit,
|
|
onDelete,
|
|
onToggleStatus,
|
|
onTestConnection,
|
|
}: 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="rounded-xl border border-2 border-dashed bg-gradient-to-br from-gray-50 to-gray-100 p-12 text-center">
|
|
<Mail className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
|
<p className="text-muted-foreground mb-2 text-lg font-medium">등록된 메일 계정이 없습니다</p>
|
|
<p className="text-muted-foreground text-sm">"새 계정 추가" 버튼을 클릭하여 첫 번째 메일 계정을 등록하세요.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 검색 */}
|
|
<div className="relative">
|
|
<Search className="absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder="계정명, 이메일, 서버로 검색..."
|
|
className="focus:ring-primary focus:border-primary w-full rounded-lg border py-3 pr-4 pl-10 transition-all focus:ring-2"
|
|
/>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="overflow-hidden rounded-xl border bg-white shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="border border-b bg-gradient-to-r from-slate-50 to-gray-50">
|
|
<tr>
|
|
<th
|
|
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-left text-sm font-semibold transition"
|
|
onClick={() => handleSort("name")}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Mail className="text-primary h-4 w-4" />
|
|
계정명
|
|
{sortField === "name" && <span className="text-xs">{sortOrder === "asc" ? "↑" : "↓"}</span>}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-left text-sm font-semibold transition"
|
|
onClick={() => handleSort("email")}
|
|
>
|
|
이메일 주소
|
|
</th>
|
|
<th className="text-foreground px-6 py-4 text-left text-sm font-semibold">SMTP 서버</th>
|
|
<th
|
|
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-center text-sm font-semibold transition"
|
|
onClick={() => handleSort("status")}
|
|
>
|
|
상태
|
|
</th>
|
|
<th
|
|
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-center text-sm font-semibold transition"
|
|
onClick={() => handleSort("dailyLimit")}
|
|
>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Zap className="text-primary h-4 w-4" />
|
|
일일 제한
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-center text-sm font-semibold transition"
|
|
onClick={() => handleSort("createdAt")}
|
|
>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Calendar className="text-primary h-4 w-4" />
|
|
생성일
|
|
</div>
|
|
</th>
|
|
<th className="text-foreground px-6 py-4 text-center text-sm font-semibold">액션</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{sortedAccounts.map((account) => (
|
|
<tr key={account.id} className="transition-colors hover:bg-orange-50/50">
|
|
<td className="px-6 py-4">
|
|
<div className="text-foreground font-medium">{account.name}</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-muted-foreground text-sm">{account.email}</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-muted-foreground text-sm">
|
|
{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 rounded-full px-3 py-1.5 text-xs font-medium transition-all hover:scale-105 ${
|
|
account.status === "active"
|
|
? "bg-green-100 text-green-700 hover:bg-green-200"
|
|
: "bg-muted text-muted-foreground hover:bg-gray-200"
|
|
}`}
|
|
>
|
|
{account.status === "active" ? (
|
|
<>
|
|
<CheckCircle className="h-3.5 w-3.5" />
|
|
활성
|
|
</>
|
|
) : (
|
|
<>
|
|
<XCircle className="h-3.5 w-3.5" />
|
|
비활성
|
|
</>
|
|
)}
|
|
</button>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<div className="text-foreground text-sm font-medium">
|
|
{account.dailyLimit > 0 ? account.dailyLimit.toLocaleString() : "무제한"}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<div className="text-muted-foreground text-sm">{formatDate(account.createdAt)}</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<button
|
|
onClick={() => onTestConnection(account)}
|
|
className="rounded-lg p-2 text-blue-600 transition-colors hover:bg-blue-50"
|
|
title="SMTP 연결 테스트"
|
|
>
|
|
<Zap className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => onEdit(account)}
|
|
className="text-primary hover:bg-accent rounded-lg p-2 transition-colors"
|
|
title="수정"
|
|
>
|
|
<Edit2 className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => onDelete(account)}
|
|
className="text-destructive hover:bg-destructive/10 rounded-lg p-2 transition-colors"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 결과 요약 */}
|
|
<div className="text-muted-foreground text-center text-sm">
|
|
전체 {accounts.length}개 중 {sortedAccounts.length}개 표시
|
|
{searchTerm && ` (검색: "${searchTerm}")`}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|