ERP-node/frontend/components/mail/MailAccountTable.tsx

229 lines
9.3 KiB
TypeScript
Raw Normal View History

2025-10-01 16:15:53 +09:00
"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";
2025-10-01 16:15:53 +09:00
interface MailAccountTableProps {
accounts: MailAccount[];
onEdit: (account: MailAccount) => void;
onDelete: (account: MailAccount) => void;
onToggleStatus: (account: MailAccount) => void;
2025-10-02 15:46:23 +09:00
onTestConnection: (account: MailAccount) => void;
2025-10-01 16:15:53 +09:00
}
export default function MailAccountTable({
accounts,
onEdit,
onDelete,
onToggleStatus,
2025-10-02 15:46:23 +09:00
onTestConnection,
2025-10-01 16:15:53 +09:00
}: MailAccountTableProps) {
const [searchTerm, setSearchTerm] = useState("");
const [sortField, setSortField] = useState<keyof MailAccount>("createdAt");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
2025-10-01 16:15:53 +09:00
// 검색 필터링
const filteredAccounts = accounts.filter(
(account) =>
account.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
account.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
account.smtpHost.toLowerCase().includes(searchTerm.toLowerCase()),
2025-10-01 16:15:53 +09:00
);
// 정렬
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);
2025-10-01 16:15:53 +09:00
}
if (typeof aValue === "number" && typeof bValue === "number") {
return sortOrder === "asc" ? aValue - bValue : bValue - aValue;
2025-10-01 16:15:53 +09:00
}
return 0;
});
const handleSort = (field: keyof MailAccount) => {
if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
2025-10-01 16:15:53 +09:00
} else {
setSortField(field);
setSortOrder("asc");
2025-10-01 16:15:53 +09:00
}
};
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",
2025-10-01 16:15:53 +09:00
});
};
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>
2025-10-01 16:15:53 +09:00
</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" />
2025-10-01 16:15:53 +09:00
<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"
2025-10-01 16:15:53 +09:00
/>
</div>
{/* 테이블 */}
<div className="overflow-hidden rounded-xl border bg-white shadow-sm">
2025-10-01 16:15:53 +09:00
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border border-b bg-gradient-to-r from-slate-50 to-gray-50">
2025-10-01 16:15:53 +09:00
<tr>
<th
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-left text-sm font-semibold transition"
onClick={() => handleSort("name")}
2025-10-01 16:15:53 +09:00
>
<div className="flex items-center gap-2">
<Mail className="text-primary h-4 w-4" />
2025-10-01 16:15:53 +09:00
{sortField === "name" && <span className="text-xs">{sortOrder === "asc" ? "↑" : "↓"}</span>}
2025-10-01 16:15:53 +09:00
</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")}
2025-10-01 16:15:53 +09:00
>
</th>
<th className="text-foreground px-6 py-4 text-left text-sm font-semibold">SMTP </th>
2025-10-01 16:15:53 +09:00
<th
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-center text-sm font-semibold transition"
onClick={() => handleSort("status")}
2025-10-01 16:15:53 +09:00
>
</th>
<th
className="text-foreground hover:bg-muted cursor-pointer px-6 py-4 text-center text-sm font-semibold transition"
onClick={() => handleSort("dailyLimit")}
2025-10-01 16:15:53 +09:00
>
<div className="flex items-center justify-center gap-2">
<Zap className="text-primary h-4 w-4" />
2025-10-01 16:15:53 +09:00
</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")}
2025-10-01 16:15:53 +09:00
>
<div className="flex items-center justify-center gap-2">
<Calendar className="text-primary h-4 w-4" />
2025-10-01 16:15:53 +09:00
</div>
</th>
<th className="text-foreground px-6 py-4 text-center text-sm font-semibold"></th>
2025-10-01 16:15:53 +09:00
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{sortedAccounts.map((account) => (
<tr key={account.id} className="transition-colors hover:bg-orange-50/50">
2025-10-01 16:15:53 +09:00
<td className="px-6 py-4">
<div className="text-foreground font-medium">{account.name}</div>
2025-10-01 16:15:53 +09:00
</td>
<td className="px-6 py-4">
<div className="text-muted-foreground text-sm">{account.email}</div>
2025-10-01 16:15:53 +09:00
</td>
<td className="px-6 py-4">
<div className="text-muted-foreground text-sm">
2025-10-01 16:15:53 +09:00
{account.smtpHost}:{account.smtpPort}
</div>
<div className="text-xs text-gray-400">{account.smtpSecure ? "SSL" : "TLS"}</div>
2025-10-01 16:15:53 +09:00
</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"
2025-10-01 16:15:53 +09:00
}`}
>
{account.status === "active" ? (
2025-10-01 16:15:53 +09:00
<>
<CheckCircle className="h-3.5 w-3.5" />
2025-10-01 16:15:53 +09:00
</>
) : (
<>
<XCircle className="h-3.5 w-3.5" />
2025-10-01 16:15:53 +09:00
</>
)}
</button>
</td>
<td className="px-6 py-4 text-center">
<div className="text-foreground text-sm font-medium">
{account.dailyLimit > 0 ? account.dailyLimit.toLocaleString() : "무제한"}
2025-10-01 16:15:53 +09:00
</div>
</td>
<td className="px-6 py-4 text-center">
<div className="text-muted-foreground text-sm">{formatDate(account.createdAt)}</div>
2025-10-01 16:15:53 +09:00
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-center gap-2">
2025-10-02 15:46:23 +09:00
<button
onClick={() => onTestConnection(account)}
className="rounded-lg p-2 text-blue-600 transition-colors hover:bg-blue-50"
2025-10-02 15:46:23 +09:00
title="SMTP 연결 테스트"
>
<Zap className="h-4 w-4" />
2025-10-02 15:46:23 +09:00
</button>
2025-10-01 16:15:53 +09:00
<button
onClick={() => onEdit(account)}
className="text-primary hover:bg-accent rounded-lg p-2 transition-colors"
2025-10-01 16:15:53 +09:00
title="수정"
>
<Edit2 className="h-4 w-4" />
2025-10-01 16:15:53 +09:00
</button>
<button
onClick={() => onDelete(account)}
className="text-destructive hover:bg-destructive/10 rounded-lg p-2 transition-colors"
2025-10-01 16:15:53 +09:00
title="삭제"
>
<Trash2 className="h-4 w-4" />
2025-10-01 16:15:53 +09:00
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 결과 요약 */}
<div className="text-muted-foreground text-center text-sm">
2025-10-01 16:15:53 +09:00
{accounts.length} {sortedAccounts.length}
{searchTerm && ` (검색: "${searchTerm}")`}
</div>
</div>
);
}