737 lines
26 KiB
TypeScript
737 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Send,
|
|
Search,
|
|
RefreshCw,
|
|
CheckCircle2,
|
|
XCircle,
|
|
Calendar,
|
|
ChevronRight,
|
|
Loader2,
|
|
Mail,
|
|
User,
|
|
Clock,
|
|
Paperclip,
|
|
Trash2,
|
|
Eye,
|
|
Reply,
|
|
Forward,
|
|
} from "lucide-react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import Link from "next/link";
|
|
import {
|
|
SentMailHistory,
|
|
getSentMailList,
|
|
deleteSentMail,
|
|
getMailAccounts,
|
|
MailAccount,
|
|
getMailStatistics,
|
|
} from "@/lib/api/mail";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import DOMPurify from "isomorphic-dompurify";
|
|
|
|
export default function SentMailPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const { toast } = useToast();
|
|
|
|
// 상태
|
|
const [mails, setMails] = useState<SentMailHistory[]>([]);
|
|
const [accounts, setAccounts] = useState<MailAccount[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
// 선택된 메일
|
|
const [selectedMailId, setSelectedMailId] = useState<string>("");
|
|
const [selectedMailDetail, setSelectedMailDetail] = useState<SentMailHistory | null>(null);
|
|
|
|
// 필터
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [filterStatus, setFilterStatus] = useState<"all" | "success" | "failed">("all");
|
|
const [filterAccountId, setFilterAccountId] = useState<string>("all");
|
|
|
|
// 페이지네이션
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [itemsPerPage] = useState(10);
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
|
|
// 통계
|
|
const [stats, setStats] = useState({
|
|
totalSent: 0,
|
|
successCount: 0,
|
|
failedCount: 0,
|
|
todayCount: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadAccounts();
|
|
loadStats();
|
|
loadMails();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setCurrentPage(1); // 필터 변경 시 첫 페이지로
|
|
loadMails();
|
|
}, [filterStatus, filterAccountId]);
|
|
|
|
useEffect(() => {
|
|
loadMails();
|
|
}, [currentPage]);
|
|
|
|
// URL에서 mailId 파라미터 확인하여 자동 선택
|
|
useEffect(() => {
|
|
const mailId = searchParams.get("mailId");
|
|
if (mailId && mails.length > 0) {
|
|
const mail = mails.find((m) => m.id === mailId);
|
|
if (mail) {
|
|
// console.log("🎯 URL에서 지정된 메일 자동 선택:", mailId);
|
|
handleMailClick(mail);
|
|
}
|
|
}
|
|
}, [searchParams, mails]);
|
|
|
|
const loadAccounts = async () => {
|
|
try {
|
|
const data = await getMailAccounts();
|
|
setAccounts(data.filter((acc) => acc.status === "active"));
|
|
} catch (error: unknown) {
|
|
const err = error as Error;
|
|
console.error("계정 로드 실패:", err);
|
|
}
|
|
};
|
|
|
|
const loadStats = async () => {
|
|
try {
|
|
const data = await getMailStatistics();
|
|
setStats({
|
|
totalSent: data.totalSent || 0,
|
|
successCount: data.successCount || 0,
|
|
failedCount: data.failedCount || 0,
|
|
todayCount: data.todaySent || 0,
|
|
});
|
|
} catch (error: unknown) {
|
|
const err = error as Error;
|
|
console.error("통계 로드 실패:", err);
|
|
}
|
|
};
|
|
|
|
const loadMails = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const filters: any = {
|
|
sortBy: "sentAt",
|
|
sortOrder: "desc",
|
|
limit: 1000, // 전체 메일 가져오기 (프론트에서 페이지네이션)
|
|
};
|
|
|
|
// 상태 필터: 'all'이면 status 필터 없음 (draft 제외하려면 success/failed만)
|
|
if (filterStatus === "all") {
|
|
// draft를 제외하고 발송된 메일만 (success + failed)
|
|
// status 필터를 안 넣으면 draft도 포함되므로, 클라이언트에서 필터링
|
|
} else {
|
|
filters.status = filterStatus; // 'success' 또는 'failed'
|
|
}
|
|
|
|
if (filterAccountId !== "all") {
|
|
filters.accountId = filterAccountId;
|
|
}
|
|
|
|
// console.log("📤 발신메일 로드 시작:", filters);
|
|
const response = await getSentMailList(filters);
|
|
// console.log("📤 발신메일 응답:", response);
|
|
|
|
let mailList = response.items || [];
|
|
|
|
// draft 제외 (발송된 메일만 표시)
|
|
mailList = mailList.filter((mail) => mail.status !== "draft");
|
|
|
|
// console.log("📤 발신메일 개수 (draft 제외):", mailList.length);
|
|
|
|
// 검색어 필터링 (클라이언트 사이드)
|
|
if (searchTerm.trim()) {
|
|
const term = searchTerm.toLowerCase();
|
|
mailList = mailList.filter(
|
|
(mail) =>
|
|
mail.subject?.toLowerCase().includes(term) ||
|
|
mail.to?.some((email) => email.toLowerCase().includes(term)) ||
|
|
mail.accountEmail?.toLowerCase().includes(term)
|
|
);
|
|
}
|
|
|
|
// 페이지네이션 적용
|
|
const totalItems = mailList.length;
|
|
const totalPagesCalc = Math.ceil(totalItems / itemsPerPage);
|
|
setTotalPages(totalPagesCalc);
|
|
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = startIndex + itemsPerPage;
|
|
const paginatedMails = mailList.slice(startIndex, endIndex);
|
|
|
|
setMails(paginatedMails);
|
|
} catch (error: unknown) {
|
|
const err = error as Error;
|
|
console.error("❌ 메일 로드 실패:", err);
|
|
toast({
|
|
title: "메일 로드 실패",
|
|
description: err.message,
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleMailClick = (mail: SentMailHistory) => {
|
|
setSelectedMailId(mail.id);
|
|
setSelectedMailDetail(mail);
|
|
// console.log("📧 메일 클릭:", mail.id);
|
|
};
|
|
|
|
const handleDeleteMail = async () => {
|
|
if (!selectedMailId || !confirm("이 메일을 삭제하시겠습니까?")) return;
|
|
|
|
try {
|
|
setDeleting(true);
|
|
await deleteSentMail(selectedMailId);
|
|
|
|
// 메일 목록에서 제거
|
|
setMails(mails.filter((m) => m.id !== selectedMailId));
|
|
|
|
// 상세 패널 닫기
|
|
setSelectedMailId("");
|
|
setSelectedMailDetail(null);
|
|
|
|
toast({
|
|
title: "메일 삭제 완료",
|
|
description: "메일이 휴지통으로 이동되었습니다.",
|
|
});
|
|
|
|
// 통계 새로고침
|
|
loadStats();
|
|
} catch (error: any) {
|
|
console.error("메일 삭제 실패:", error);
|
|
toast({
|
|
title: "메일 삭제 실패",
|
|
description: error.response?.data?.message || error.message,
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setDeleting(false);
|
|
}
|
|
};
|
|
|
|
const handleReply = () => {
|
|
if (!selectedMailDetail) return;
|
|
|
|
// 원본 수신자를 받는사람으로 설정
|
|
const toEmail = selectedMailDetail.to?.[0] || "";
|
|
|
|
const replyData = {
|
|
originalFrom: selectedMailDetail.accountEmail,
|
|
originalTo: toEmail,
|
|
originalSubject: selectedMailDetail.subject,
|
|
originalDate: selectedMailDetail.sentAt,
|
|
originalBody: selectedMailDetail.htmlContent || "",
|
|
};
|
|
|
|
router.push(
|
|
`/admin/mail/send?action=reply&data=${encodeURIComponent(JSON.stringify(replyData))}`
|
|
);
|
|
};
|
|
|
|
const handleForward = () => {
|
|
if (!selectedMailDetail) return;
|
|
|
|
const forwardData = {
|
|
originalFrom: selectedMailDetail.accountEmail,
|
|
originalSubject: selectedMailDetail.subject,
|
|
originalDate: selectedMailDetail.sentAt,
|
|
originalBody: selectedMailDetail.htmlContent || "",
|
|
};
|
|
|
|
router.push(
|
|
`/admin/mail/send?action=forward&data=${encodeURIComponent(JSON.stringify(forwardData))}`
|
|
);
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diff = now.getTime() - date.getTime();
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
|
|
if (hours < 24) {
|
|
return date.toLocaleTimeString("ko-KR", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
} else {
|
|
return date.toLocaleDateString("ko-KR", {
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 필터링된 메일 개수
|
|
const filteredCount = mails.length;
|
|
|
|
if (loading && mails.length === 0) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
<p className="text-sm text-muted-foreground">메일을 불러오는 중...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-6 bg-background min-h-screen">
|
|
{/* 헤더 */}
|
|
<div className="bg-card rounded-lg border p-6 space-y-4">
|
|
{/* 브레드크럼브 */}
|
|
<nav className="flex items-center gap-2 text-sm">
|
|
<Link
|
|
href="/admin/mail/dashboard"
|
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
메일 관리
|
|
</Link>
|
|
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
|
<span className="text-foreground font-medium">보낸메일함</span>
|
|
</nav>
|
|
|
|
<Separator />
|
|
|
|
{/* 제목 및 빠른 액션 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground flex items-center gap-2">
|
|
<Send className="w-8 h-8" />
|
|
보낸메일함
|
|
</h1>
|
|
<p className="mt-2 text-muted-foreground">
|
|
총 {filteredCount}개의 발송 메일
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button onClick={loadMails} variant="outline" size="sm" disabled={loading}>
|
|
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
|
새로고침
|
|
</Button>
|
|
<Button onClick={() => router.push("/admin/automaticMng/mail/send")} size="sm">
|
|
<Mail className="w-4 h-4 mr-2" />
|
|
메일 작성
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 통계 카드 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">전체 발송</p>
|
|
<p className="text-2xl font-bold text-foreground mt-1">
|
|
{stats.totalSent}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-primary/10 rounded-lg">
|
|
<Send className="w-6 h-6 text-primary" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">발송 성공</p>
|
|
<p className="text-2xl font-bold text-foreground mt-1">
|
|
{stats.successCount}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-green-500/10 rounded-lg">
|
|
<CheckCircle2 className="w-6 h-6 text-green-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">발송 실패</p>
|
|
<p className="text-2xl font-bold text-foreground mt-1">
|
|
{stats.failedCount}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-red-500/10 rounded-lg">
|
|
<XCircle className="w-6 h-6 text-red-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">오늘 발송</p>
|
|
<p className="text-2xl font-bold text-foreground mt-1">
|
|
{stats.todayCount}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-blue-500/10 rounded-lg">
|
|
<Calendar className="w-6 h-6 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 검색 및 필터 */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
{/* 검색 */}
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="제목, 받는사람, 계정으로 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 상태 필터 */}
|
|
<Select value={filterStatus} onValueChange={(value: any) => setFilterStatus(value)}>
|
|
<SelectTrigger className="w-full sm:w-[180px]">
|
|
<SelectValue placeholder="상태" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체</SelectItem>
|
|
<SelectItem value="success">발송 성공</SelectItem>
|
|
<SelectItem value="failed">발송 실패</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 계정 필터 */}
|
|
<Select
|
|
value={filterAccountId}
|
|
onValueChange={(value) => setFilterAccountId(value)}
|
|
>
|
|
<SelectTrigger className="w-full sm:w-[200px]">
|
|
<SelectValue placeholder="발송 계정" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체 계정</SelectItem>
|
|
{accounts.map((account) => (
|
|
<SelectItem key={account.id} value={account.id}>
|
|
{account.name} ({account.email})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 메일 목록 + 상세보기 */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* 왼쪽: 메일 목록 */}
|
|
<div className="lg:col-span-1">
|
|
<Card className="flex flex-col h-[calc(100vh-500px)] min-h-[400px]">
|
|
<CardHeader className="flex-shrink-0">
|
|
<CardTitle className="text-base">메일 목록</CardTitle>
|
|
<CardDescription>{filteredCount}개의 메일</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex-1 p-0 overflow-hidden">
|
|
<div className="h-full overflow-y-auto">
|
|
{mails.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
|
<Mail className="w-12 h-12 text-muted-foreground mb-4" />
|
|
<p className="text-sm text-muted-foreground">
|
|
발송된 메일이 없습니다
|
|
</p>
|
|
</div>
|
|
) : (
|
|
mails.map((mail) => (
|
|
<div
|
|
key={mail.id}
|
|
onClick={() => handleMailClick(mail)}
|
|
className={`
|
|
p-4 border-b cursor-pointer transition-colors
|
|
hover:bg-accent
|
|
${selectedMailId === mail.id ? "bg-accent" : ""}
|
|
`}
|
|
>
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<User className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
<span className="text-sm font-medium truncate">
|
|
{mail.to?.[0] || "받는사람 없음"}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm font-semibold truncate">
|
|
{mail.subject || "(제목 없음)"}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-1 flex-shrink-0">
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatDate(mail.sentAt)}
|
|
</span>
|
|
{mail.status === "success" ? (
|
|
<Badge variant="default" className="text-xs">
|
|
<CheckCircle2 className="w-3 h-3 mr-1" />
|
|
성공
|
|
</Badge>
|
|
) : mail.status === "failed" ? (
|
|
<Badge variant="destructive" className="text-xs">
|
|
<XCircle className="w-3 h-3 mr-1" />
|
|
실패
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
<Mail className="w-3 h-3" />
|
|
<span className="truncate">{mail.accountEmail}</span>
|
|
{mail.attachments && mail.attachments.length > 0 && (
|
|
<>
|
|
<Paperclip className="w-3 h-3 ml-2" />
|
|
<span>{mail.attachments.length}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
|
|
{/* 페이지네이션 */}
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-center gap-2 p-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setCurrentPage(1)}
|
|
disabled={currentPage === 1}
|
|
>
|
|
처음
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
|
disabled={currentPage === 1}
|
|
>
|
|
이전
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
let pageNum;
|
|
if (totalPages <= 5) {
|
|
pageNum = i + 1;
|
|
} else if (currentPage <= 3) {
|
|
pageNum = i + 1;
|
|
} else if (currentPage >= totalPages - 2) {
|
|
pageNum = totalPages - 4 + i;
|
|
} else {
|
|
pageNum = currentPage - 2 + i;
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
key={pageNum}
|
|
variant={currentPage === pageNum ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setCurrentPage(pageNum)}
|
|
className="w-8 h-8 p-0"
|
|
>
|
|
{pageNum}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
|
disabled={currentPage === totalPages}
|
|
>
|
|
다음
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setCurrentPage(totalPages)}
|
|
disabled={currentPage === totalPages}
|
|
>
|
|
마지막
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 오른쪽: 메일 상세보기 */}
|
|
<div className="lg:col-span-2">
|
|
{selectedMailDetail ? (
|
|
<Card className="flex flex-col h-[calc(100vh-500px)] min-h-[400px]">
|
|
<CardHeader className="flex-shrink-0">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<CardTitle className="text-xl mb-2">
|
|
{selectedMailDetail.subject || "(제목 없음)"}
|
|
</CardTitle>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<User className="w-4 h-4 text-muted-foreground" />
|
|
<span className="font-medium">보낸사람:</span>
|
|
<span className="text-muted-foreground">
|
|
{selectedMailDetail.accountName} ({selectedMailDetail.accountEmail})
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Mail className="w-4 h-4 text-muted-foreground" />
|
|
<span className="font-medium">받는사람:</span>
|
|
<span className="text-muted-foreground">
|
|
{selectedMailDetail.to?.join(", ") || "-"}
|
|
</span>
|
|
</div>
|
|
{selectedMailDetail.cc && selectedMailDetail.cc.length > 0 && (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Mail className="w-4 h-4 text-muted-foreground" />
|
|
<span className="font-medium">참조:</span>
|
|
<span className="text-muted-foreground">
|
|
{selectedMailDetail.cc.join(", ")}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Clock className="w-4 h-4 text-muted-foreground" />
|
|
<span className="font-medium">발송일시:</span>
|
|
<span className="text-muted-foreground">
|
|
{new Date(selectedMailDetail.sentAt).toLocaleString("ko-KR")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 답장/전달/삭제 버튼 */}
|
|
<div className="flex gap-2 mt-4">
|
|
<Button variant="outline" size="sm" onClick={handleReply}>
|
|
<Reply className="w-4 h-4 mr-1" />
|
|
답장
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={handleForward}>
|
|
<Forward className="w-4 h-4 mr-1" />
|
|
전달
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={handleDeleteMail}
|
|
disabled={deleting}
|
|
>
|
|
{deleting ? (
|
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
|
) : (
|
|
<Trash2 className="w-4 h-4 mr-1" />
|
|
)}
|
|
삭제
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<Separator className="flex-shrink-0" />
|
|
|
|
<CardContent className="flex-1 overflow-y-auto pt-6">
|
|
{/* 첨부파일 */}
|
|
{selectedMailDetail.attachments && selectedMailDetail.attachments.length > 0 && (
|
|
<div className="mb-6 p-4 bg-muted rounded-lg">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Paperclip className="w-4 h-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">
|
|
첨부파일 ({selectedMailDetail.attachments.length})
|
|
</span>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{selectedMailDetail.attachments.map((file: any, index: number) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-2 text-sm bg-background p-2 rounded"
|
|
>
|
|
<Paperclip className="w-4 h-4 text-muted-foreground" />
|
|
<span className="flex-1 truncate">{file.filename || file.name}</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{file.size ? `${(file.size / 1024).toFixed(1)}KB` : ""}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 메일 본문 */}
|
|
{selectedMailDetail.htmlContent ? (
|
|
<div
|
|
className="prose prose-sm max-w-none"
|
|
dangerouslySetInnerHTML={{
|
|
__html: DOMPurify.sanitize(selectedMailDetail.htmlContent),
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="whitespace-pre-wrap text-sm">
|
|
{selectedMailDetail.htmlContent || "(내용 없음)"}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="h-[calc(100vh-500px)] min-h-[400px]">
|
|
<CardContent className="flex flex-col items-center justify-center h-full">
|
|
<Eye className="w-16 h-16 text-muted-foreground mb-4" />
|
|
<p className="text-muted-foreground">
|
|
메일을 선택하면 내용이 표시됩니다
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|