ERP-node/frontend/app/(main)/admin/automaticMng/mail/receive/page.tsx

950 lines
38 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Inbox,
Mail,
RefreshCw,
Loader2,
CheckCircle,
Paperclip,
AlertCircle,
Search,
Filter,
SortAsc,
SortDesc,
ChevronRight,
Reply,
Forward,
Trash2,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { Separator } from "@/components/ui/separator";
import {
MailAccount,
ReceivedMail,
MailDetail,
getMailAccounts,
getReceivedMails,
testImapConnection,
getMailDetail,
markMailAsRead,
downloadMailAttachment,
} from "@/lib/api/mail";
import { apiClient } from "@/lib/api/client";
import DOMPurify from "isomorphic-dompurify";
export default function MailReceivePage() {
const router = useRouter();
const searchParams = useSearchParams();
const [accounts, setAccounts] = useState<MailAccount[]>([]);
const [selectedAccountId, setSelectedAccountId] = useState<string>("");
const [mails, setMails] = useState<ReceivedMail[]>([]);
const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
// 메일 상세 상태 (모달 대신 패널)
const [selectedMailId, setSelectedMailId] = useState<string>("");
const [selectedMailDetail, setSelectedMailDetail] = useState<MailDetail | null>(null);
const [loadingDetail, setLoadingDetail] = useState(false);
const [deleting, setDeleting] = useState(false);
// 검색 및 필터 상태
const [searchTerm, setSearchTerm] = useState<string>("");
const [filterStatus, setFilterStatus] = useState<string>("all"); // all, unread, read, attachment
const [sortBy, setSortBy] = useState<string>("date-desc"); // date-desc, date-asc, from-asc, from-desc
// 페이지네이션
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [totalPages, setTotalPages] = useState(1);
const [allMails, setAllMails] = useState<ReceivedMail[]>([]); // 전체 메일 저장
// 계정 목록 로드
useEffect(() => {
loadAccounts();
}, []);
// 계정 선택 시 메일 로드
useEffect(() => {
if (selectedAccountId) {
setCurrentPage(1); // 계정 변경 시 첫 페이지로
loadMails();
}
}, [selectedAccountId]);
// 페이지 변경 시 페이지네이션 재적용
useEffect(() => {
if (allMails.length > 0) {
applyPagination(allMails);
}
}, [currentPage]);
// URL 파라미터에서 mailId 읽기 및 자동 선택
useEffect(() => {
const mailId = searchParams.get("mailId");
const accountId = searchParams.get("accountId");
if (mailId && accountId) {
// console.log('📧 URL에서 메일 ID 감지:', mailId, accountId);
setSelectedAccountId(accountId);
setSelectedMailId(mailId);
// 메일 상세 로드는 handleMailClick에서 처리됨
}
}, [searchParams]);
// 메일 목록 로드 후 URL에서 지정된 메일 자동 선택
useEffect(() => {
if (selectedMailId && mails.length > 0 && !selectedMailDetail) {
const mail = mails.find((m) => m.id === selectedMailId);
if (mail) {
// console.log('🎯 URL에서 지정된 메일 자동 선택:', selectedMailId);
handleMailClick(mail);
}
}
}, [mails, selectedMailId, selectedMailDetail]); // selectedMailDetail 추가로 무한 루프 방지
// 자동 새로고침 (30초마다)
useEffect(() => {
if (!selectedAccountId) return;
const interval = setInterval(() => {
loadMails();
}, 30000); // 30초
return () => clearInterval(interval);
}, [selectedAccountId]);
const loadAccounts = async () => {
try {
const data = await getMailAccounts();
if (Array.isArray(data)) {
const activeAccounts = data.filter((acc) => acc.status === "active");
setAccounts(activeAccounts);
if (activeAccounts.length > 0 && !selectedAccountId) {
setSelectedAccountId(activeAccounts[0].id);
}
}
} catch (error) {
// console.error("계정 로드 실패:", error);
}
};
const loadMails = async () => {
if (!selectedAccountId) return;
setLoading(true);
setTestResult(null);
try {
const data = await getReceivedMails(selectedAccountId, 200); // 더 많이 가져오기
// 현재 로컬에서 읽음 처리한 메일들의 상태를 유지
const processedMails = data.map((mail) => ({
...mail,
isRead: mail.isRead,
}));
setAllMails(processedMails); // 전체 메일 저장
// 페이지네이션 적용
applyPagination(processedMails);
// 알림 갱신 이벤트 발생 (새 메일이 있을 수 있음)
window.dispatchEvent(new CustomEvent("mail-received"));
} catch (error) {
// console.error("메일 로드 실패:", error);
alert(error instanceof Error ? error.message : "메일을 불러오는데 실패했습니다.");
setMails([]);
setAllMails([]);
} finally {
setLoading(false);
}
};
const applyPagination = (mailList: ReceivedMail[]) => {
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);
};
const handleTestConnection = async () => {
if (!selectedAccountId) return;
setTesting(true);
setTestResult(null);
try {
const result = await testImapConnection(selectedAccountId);
setTestResult(result);
if (result.success) {
// 연결 성공 후 자동으로 메일 로드
setTimeout(() => loadMails(), 1000);
}
} catch (error) {
setTestResult({
success: false,
message: error instanceof Error ? error.message : "IMAP 연결 테스트 실패",
});
} finally {
setTesting(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 60) {
return `${diffMins}분 전`;
} else if (diffHours < 24) {
return `${diffHours}시간 전`;
} else if (diffDays < 7) {
return `${diffDays}일 전`;
} else {
return date.toLocaleDateString("ko-KR");
}
};
const handleMailClick = async (mail: ReceivedMail) => {
setSelectedMailId(mail.id);
setLoadingDetail(true);
// 즉시 로컬 상태 업데이트 (UI 반응성 향상)
// console.log('📧 메일 클릭:', mail.id, '현재 읽음 상태:', mail.isRead);
setMails((prevMails) => prevMails.map((m) => (m.id === mail.id ? { ...m, isRead: true } : m)));
// 메일 상세 정보 로드
try {
// mail.id에서 accountId와 seqno 추출: "account-{timestamp}-{seqno}" 형식
const mailIdParts = mail.id.split("-");
const accountId = `${mailIdParts[0]}-${mailIdParts[1]}`; // "account-1759310844272"
const seqno = parseInt(mailIdParts[2], 10); // 13
// console.log('🔍 추출된 accountId:', accountId, 'seqno:', seqno, '원본 mailId:', mail.id);
const detail = await getMailDetail(accountId, seqno);
setSelectedMailDetail(detail);
// 읽음 처리
if (!mail.isRead) {
await markMailAsRead(accountId, seqno);
// console.log('✅ 읽음 처리 완료 - seqno:', seqno);
// 서버 상태 동기화 (백그라운드) - IMAP 서버 반영 대기
setTimeout(() => {
if (selectedAccountId) {
// console.log('🔄 서버 상태 동기화 시작');
loadMails();
}
}, 2000); // 2초로 증가
}
} catch (error) {
// console.error('메일 상세 로드 실패:', error);
} finally {
setLoadingDetail(false);
}
};
const handleDeleteMail = async () => {
if (
!selectedMailId ||
!confirm(
"이 메일을 IMAP 서버에서 삭제하시겠습니까?\n(Gmail/Naver 휴지통으로 이동됩니다)\n\n⚠ IMAP 연결에 시간이 걸릴 수 있습니다.",
)
)
return;
try {
setDeleting(true);
// mail.id에서 accountId와 seqno 추출: "account-{timestamp}-{seqno}" 형식
const mailIdParts = selectedMailId.split("-");
const accountId = `${mailIdParts[0]}-${mailIdParts[1]}`; // "account-1759310844272"
const seqno = parseInt(mailIdParts[2], 10); // 10
// console.log(`🗑️ 메일 삭제 시도: accountId=${accountId}, seqno=${seqno}`);
// IMAP 서버에서 메일 삭제 (타임아웃 40초)
const response = await apiClient.delete(`/mail/receive/${accountId}/${seqno}`, {
timeout: 40000, // 40초 타임아웃
});
if (response.data.success) {
// 메일 목록에서 제거
setMails(mails.filter((m) => m.id !== selectedMailId));
// 상세 패널 닫기
setSelectedMailId("");
setSelectedMailDetail(null);
alert("메일이 삭제되었습니다.");
// console.log("✅ 메일 삭제 완료");
}
} catch (error: any) {
// console.error("메일 삭제 실패:", error);
let errorMessage = "메일 삭제에 실패했습니다.";
if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
errorMessage = "IMAP 서버 연결 시간 초과\n네트워크 상태를 확인하거나 나중에 다시 시도해주세요.";
} else if (error.response?.data?.message) {
errorMessage = error.response.data.message;
}
alert(errorMessage);
} finally {
setDeleting(false);
}
};
// 필터링 및 정렬된 메일 목록
const filteredAndSortedMails = React.useMemo(() => {
let result = [...mails];
// 검색
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
result = result.filter(
(mail) =>
mail.subject.toLowerCase().includes(searchLower) ||
mail.from.toLowerCase().includes(searchLower) ||
mail.preview.toLowerCase().includes(searchLower),
);
}
// 필터
if (filterStatus === "unread") {
result = result.filter((mail) => !mail.isRead);
} else if (filterStatus === "read") {
result = result.filter((mail) => mail.isRead);
} else if (filterStatus === "attachment") {
result = result.filter((mail) => mail.hasAttachments);
}
// 정렬
if (sortBy === "date-desc") {
result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
} else if (sortBy === "date-asc") {
result.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
} else if (sortBy === "from-asc") {
result.sort((a, b) => a.from.localeCompare(b.from));
} else if (sortBy === "from-desc") {
result.sort((a, b) => b.from.localeCompare(a.from));
}
return result;
}, [mails, searchTerm, filterStatus, sortBy]);
return (
<div className="bg-background min-h-screen">
<div className="w-full max-w-none space-y-8 px-4 py-8">
{/* 페이지 제목 */}
<div className="bg-card space-y-4 rounded-lg border p-6">
{/* 브레드크럼브 */}
<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="text-muted-foreground h-4 w-4" />
<span className="text-foreground font-medium"> </span>
</nav>
<Separator />
{/* 제목 + 액션 버튼들 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-foreground text-3xl font-bold"> </h1>
<p className="text-muted-foreground mt-2">IMAP으로 </p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={loadMails} disabled={loading || !selectedAccountId}>
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleTestConnection}
disabled={testing || !selectedAccountId}
>
{testing ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <CheckCircle className="mr-2 h-4 w-4" />}
</Button>
</div>
</div>
</div>
{/* 계정 선택 */}
<Card className="">
<CardContent className="p-4">
<div className="flex items-center gap-4">
<label className="text-foreground text-sm font-medium whitespace-nowrap"> :</label>
<select
value={selectedAccountId}
onChange={(e) => setSelectedAccountId(e.target.value)}
className="flex-1 rounded-lg border px-4 py-2 focus:border-orange-500 focus:ring-2 focus:ring-orange-500"
>
<option value=""> </option>
{accounts.map((account) => (
<option key={account.id} value={account.id}>
{account.name} ({account.email})
</option>
))}
</select>
</div>
{/* 연결 테스트 결과 */}
{testResult && (
<div
className={`mt-4 flex items-center gap-2 rounded-lg p-3 ${
testResult.success
? "border border-green-200 bg-green-50 text-green-800"
: "border border-red-200 bg-red-50 text-red-800"
}`}
>
{testResult.success ? <CheckCircle className="h-5 w-5" /> : <AlertCircle className="h-5 w-5" />}
<span>{testResult.message}</span>
</div>
)}
</CardContent>
</Card>
{/* 검색 및 필터 */}
{selectedAccountId && (
<Card className="">
<CardContent className="p-4">
<div className="flex flex-col gap-3 md:flex-row">
{/* 검색 */}
<div className="relative flex-1">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="제목, 발신자, 내용으로 검색..."
className="w-full rounded-lg border py-2 pr-4 pl-10 focus:border-orange-500 focus:ring-2 focus:ring-orange-500"
/>
</div>
{/* 필터 */}
<div className="flex items-center gap-2">
<Filter className="text-muted-foreground h-4 w-4" />
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="rounded-lg border px-3 py-2 focus:border-orange-500 focus:ring-2 focus:ring-orange-500"
>
<option value="all"></option>
<option value="unread"> </option>
<option value="read"></option>
<option value="attachment"> </option>
</select>
</div>
{/* 정렬 */}
<div className="flex items-center gap-2">
{sortBy.includes("desc") ? (
<SortDesc className="text-muted-foreground h-4 w-4" />
) : (
<SortAsc className="text-muted-foreground h-4 w-4" />
)}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="rounded-lg border px-3 py-2 focus:border-orange-500 focus:ring-2 focus:ring-orange-500"
>
<option value="date-desc"> ()</option>
<option value="date-asc"> ()</option>
<option value="from-asc"> (A-Z)</option>
<option value="from-desc"> (Z-A)</option>
</select>
</div>
</div>
{/* 검색 결과 카운트 */}
{(searchTerm || filterStatus !== "all") && (
<div className="text-muted-foreground mt-3 text-sm">
{filteredAndSortedMails.length}
{searchTerm && (
<span className="ml-2">
(: <span className="font-medium text-orange-600">{searchTerm}</span>)
</span>
)}
</div>
)}
</CardContent>
</Card>
)}
{/* 네이버 메일 스타일 3-column 레이아웃 */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* 왼쪽: 메일 목록 */}
<div className="lg:col-span-1">
{loading ? (
<Card className="">
<CardContent className="flex items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-orange-500" />
<span className="text-muted-foreground ml-3"> ...</span>
</CardContent>
</Card>
) : filteredAndSortedMails.length === 0 ? (
<Card className="bg-card py-16 text-center">
<CardContent className="pt-6">
<Mail className="mx-auto mb-4 h-16 w-16 text-gray-300" />
<p className="text-muted-foreground mb-4">
{!selectedAccountId
? "메일 계정을 선택하세요"
: searchTerm || filterStatus !== "all"
? "검색 결과가 없습니다"
: "받은 메일이 없습니다"}
</p>
{selectedAccountId && (
<Button onClick={handleTestConnection} variant="outline" disabled={testing}>
{testing ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<CheckCircle className="mr-2 h-4 w-4" />
)}
IMAP
</Button>
)}
</CardContent>
</Card>
) : (
<Card className="">
<CardHeader className="border-b bg-gradient-to-r from-slate-50 to-gray-50">
<CardTitle className="flex items-center gap-2">
<Inbox className="h-5 w-5 text-orange-500" />
({filteredAndSortedMails.length}/{mails.length})
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="max-h-[calc(100vh-300px)] divide-y overflow-y-auto">
{filteredAndSortedMails.map((mail) => (
<div
key={mail.id}
onClick={() => handleMailClick(mail)}
className={`hover:bg-background cursor-pointer p-4 transition-colors ${
!mail.isRead ? "bg-blue-50/30" : ""
} ${selectedMailId === mail.id ? "bg-accent border-l-primary border-l-4" : ""}`}
>
<div className="flex items-start gap-4">
{/* 읽음 표시 */}
<div className="mt-2 h-2 w-2 flex-shrink-0">
{!mail.isRead && <div className="h-2 w-2 rounded-full bg-blue-500"></div>}
</div>
{/* 메일 내용 */}
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center justify-between">
<span
className={`text-sm ${
mail.isRead ? "text-muted-foreground" : "text-foreground font-semibold"
}`}
>
{mail.from}
</span>
<div className="flex items-center gap-2">
{mail.hasAttachments && <Paperclip className="h-4 w-4 text-gray-400" />}
<span className="text-muted-foreground text-xs">{formatDate(mail.date)}</span>
</div>
</div>
<h3
className={`mb-1 truncate text-sm ${
mail.isRead ? "text-foreground" : "text-foreground font-medium"
}`}
>
{mail.subject}
</h3>
<p className="text-muted-foreground line-clamp-2 text-xs">{mail.preview}</p>
</div>
</div>
</div>
))}
</div>
</CardContent>
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 border-t p-4">
<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="h-8 w-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-1">
{selectedMailDetail ? (
<Card className="sticky top-6">
<CardHeader className="border-b">
<div className="flex items-center justify-between">
<CardTitle className="text-lg">{selectedMailDetail.subject}</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedMailId("");
setSelectedMailDetail(null);
}}
>
</Button>
</div>
<div className="text-muted-foreground mt-2 space-y-1 text-sm">
<div className="flex items-center gap-2">
<span className="font-medium"> :</span>
<span>{selectedMailDetail.from}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium"> :</span>
<span>{selectedMailDetail.to}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">:</span>
<span>{new Date(selectedMailDetail.date).toLocaleString("ko-KR")}</span>
</div>
</div>
{/* 답장/전달/삭제 버튼 */}
<div className="mt-4 flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
// HTML 태그 제거 함수 (강력한 버전)
const stripHtml = (html: string) => {
if (!html) return "";
// 1. DOMPurify로 먼저 정제
const cleanHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [], // 모든 태그 제거
KEEP_CONTENT: true, // 내용만 유지
});
// 2. DOM으로 텍스트만 추출
const tmp = document.createElement("DIV");
tmp.innerHTML = cleanHtml;
let text = tmp.textContent || tmp.innerText || "";
// 3. CSS 스타일 제거 (p{...} 같은 패턴)
text = text.replace(/[a-z-]+\{[^}]*\}/gi, "");
// 4. 연속된 공백 정리
text = text.replace(/\s+/g, " ").trim();
return text;
};
// console.log('📧 답장 데이터:', {
// htmlBody: selectedMailDetail.htmlBody,
// textBody: selectedMailDetail.textBody,
// });
// textBody 우선 사용 (순수 텍스트), 없으면 htmlBody에서 추출
const bodyText =
selectedMailDetail.textBody ||
(selectedMailDetail.htmlBody ? stripHtml(selectedMailDetail.htmlBody) : "");
// console.log('📧 변환된 본문:', bodyText);
const replyData = {
originalFrom: selectedMailDetail.from,
originalSubject: selectedMailDetail.subject,
originalDate: selectedMailDetail.date,
originalBody: bodyText,
};
router.push(
`/admin/mail/send?action=reply&data=${encodeURIComponent(JSON.stringify(replyData))}`,
);
}}
>
<Reply className="mr-1 h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
// HTML 태그 제거 함수 (강력한 버전)
const stripHtml = (html: string) => {
if (!html) return "";
// 1. DOMPurify로 먼저 정제
const cleanHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [], // 모든 태그 제거
KEEP_CONTENT: true, // 내용만 유지
});
// 2. DOM으로 텍스트만 추출
const tmp = document.createElement("DIV");
tmp.innerHTML = cleanHtml;
let text = tmp.textContent || tmp.innerText || "";
// 3. CSS 스타일 제거 (p{...} 같은 패턴)
text = text.replace(/[a-z-]+\{[^}]*\}/gi, "");
// 4. 연속된 공백 정리
text = text.replace(/\s+/g, " ").trim();
return text;
};
// console.log('📧 전달 데이터:', {
// htmlBody: selectedMailDetail.htmlBody,
// textBody: selectedMailDetail.textBody,
// });
// textBody 우선 사용 (순수 텍스트), 없으면 htmlBody에서 추출
const bodyText =
selectedMailDetail.textBody ||
(selectedMailDetail.htmlBody ? stripHtml(selectedMailDetail.htmlBody) : "");
// console.log('📧 변환된 본문:', bodyText);
const forwardData = {
originalFrom: selectedMailDetail.from,
originalSubject: selectedMailDetail.subject,
originalDate: selectedMailDetail.date,
originalBody: bodyText,
};
router.push(
`/admin/mail/send?action=forward&data=${encodeURIComponent(JSON.stringify(forwardData))}`,
);
}}
>
<Forward className="mr-1 h-4 w-4" />
</Button>
<Button variant="destructive" size="sm" onClick={handleDeleteMail} disabled={deleting}>
{deleting ? (
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
) : (
<Trash2 className="mr-1 h-4 w-4" />
)}
</Button>
</div>
</CardHeader>
<CardContent className="max-h-[calc(100vh-300px)] overflow-y-auto p-6">
{/* 첨부파일 */}
{selectedMailDetail.attachments && selectedMailDetail.attachments.length > 0 && (
<div className="bg-muted mb-4 rounded-lg p-3">
<p className="mb-2 text-sm font-medium"> ({selectedMailDetail.attachments.length})</p>
<div className="space-y-1">
{selectedMailDetail.attachments.map((att, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<Paperclip className="h-4 w-4" />
<span>{att.filename}</span>
<span className="text-muted-foreground">({(att.size / 1024).toFixed(1)} KB)</span>
</div>
))}
</div>
</div>
)}
{/* 메일 본문 */}
{selectedMailDetail.htmlBody ? (
<div
className="prose prose-sm max-w-none"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(selectedMailDetail.htmlBody),
}}
/>
) : (
<div className="text-sm whitespace-pre-wrap">{selectedMailDetail.textBody}</div>
)}
</CardContent>
</Card>
) : loadingDetail ? (
<Card className="sticky top-6">
<CardContent className="flex items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-orange-500" />
<span className="text-muted-foreground ml-3"> ...</span>
</CardContent>
</Card>
) : (
<Card className="sticky top-6">
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
<Mail className="mb-4 h-16 w-16 text-gray-300" />
<p className="text-muted-foreground"> </p>
</CardContent>
</Card>
)}
</div>
</div>
{/* 안내 정보 */}
<Card className="border-green-200 bg-gradient-to-r from-green-50 to-emerald-50">
<CardHeader>
<CardTitle className="flex items-center text-lg">
<CheckCircle className="mr-2 h-5 w-5 text-green-600" />
! 🎉
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-foreground mb-4"> :</p>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div>
<p className="mb-2 font-medium text-gray-800">📬 </p>
<ul className="text-muted-foreground space-y-1 text-sm">
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span>IMAP </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span>/ </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
</ul>
</div>
<div>
<p className="mb-2 font-medium text-gray-800">📄 </p>
<ul className="text-muted-foreground space-y-1 text-sm">
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span>HTML </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
</ul>
</div>
<div>
<p className="mb-2 font-medium text-gray-800">🔍 </p>
<ul className="text-muted-foreground space-y-1 text-sm">
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> (//)</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> (/)</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> (/)</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> (30)</span>
</li>
</ul>
</div>
<div>
<p className="mb-2 font-medium text-gray-800">🔒 </p>
<ul className="text-muted-foreground space-y-1 text-sm">
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span>XSS (DOMPurify)</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
<li className="flex items-start">
<span className="mr-2 text-green-500"></span>
<span> </span>
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}