"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([]); const [selectedAccountId, setSelectedAccountId] = useState(""); const [mails, setMails] = useState([]); const [loading, setLoading] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string; } | null>(null); // 메일 상세 상태 (모달 대신 패널) const [selectedMailId, setSelectedMailId] = useState(""); const [selectedMailDetail, setSelectedMailDetail] = useState(null); const [loadingDetail, setLoadingDetail] = useState(false); const [deleting, setDeleting] = useState(false); // 검색 및 필터 상태 const [searchTerm, setSearchTerm] = useState(""); const [filterStatus, setFilterStatus] = useState("all"); // all, unread, read, attachment const [sortBy, setSortBy] = useState("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([]); // 전체 메일 저장 // 계정 목록 로드 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 (
{/* 페이지 제목 */}
{/* 브레드크럼브 */} {/* 제목 + 액션 버튼들 */}

메일 수신함

IMAP으로 받은 메일을 확인합니다

{/* 계정 선택 */}
{/* 연결 테스트 결과 */} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* 검색 및 필터 */} {selectedAccountId && (
{/* 검색 */}
setSearchTerm(e.target.value)} placeholder="제목, 발신자, 내용으로 검색..." className="w-full pl-10 pr-4 py-2 border border rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500" />
{/* 필터 */}
{/* 정렬 */}
{sortBy.includes("desc") ? ( ) : ( )}
{/* 검색 결과 카운트 */} {(searchTerm || filterStatus !== "all") && (
{filteredAndSortedMails.length}개의 메일이 검색되었습니다 {searchTerm && ( (검색어: {searchTerm}) )}
)}
)} {/* 네이버 메일 스타일 3-column 레이아웃 */}
{/* 왼쪽: 메일 목록 */}
{loading ? ( 메일을 불러오는 중... ) : filteredAndSortedMails.length === 0 ? (

{!selectedAccountId ? "메일 계정을 선택하세요" : searchTerm || filterStatus !== "all" ? "검색 결과가 없습니다" : "받은 메일이 없습니다"}

{selectedAccountId && ( )}
) : ( 받은 메일함 ({filteredAndSortedMails.length}/{mails.length}개)
{filteredAndSortedMails.map((mail) => (
handleMailClick(mail)} className={`p-4 hover:bg-background transition-colors cursor-pointer ${ !mail.isRead ? "bg-blue-50/30" : "" } ${selectedMailId === mail.id ? "bg-accent border-l-4 border-l-primary" : ""}`} >
{/* 읽음 표시 */}
{!mail.isRead && (
)}
{/* 메일 내용 */}
{mail.from}
{mail.hasAttachments && ( )} {formatDate(mail.date)}

{mail.subject}

{mail.preview}

))}
{/* 페이지네이션 */} {totalPages > 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 ( ); })}
)}
)}
{/* 오른쪽: 메일 상세 패널 */}
{selectedMailDetail ? (
{selectedMailDetail.subject}
보낸 사람: {selectedMailDetail.from}
받는 사람: {selectedMailDetail.to}
날짜: {new Date(selectedMailDetail.date).toLocaleString("ko-KR")}
{/* 답장/전달/삭제 버튼 */}
{/* 첨부파일 */} {selectedMailDetail.attachments && selectedMailDetail.attachments.length > 0 && (

첨부파일 ({selectedMailDetail.attachments.length}개)

{selectedMailDetail.attachments.map((att, index) => (
{att.filename} ({(att.size / 1024).toFixed(1)} KB)
))}
)} {/* 메일 본문 */} {selectedMailDetail.htmlBody ? (
) : (
{selectedMailDetail.textBody}
)} ) : loadingDetail ? ( 메일을 불러오는 중... ) : (

메일을 선택하면 내용이 표시됩니다

)}
{/* 안내 정보 */} 메일 수신 기능 완성! 🎉

✅ 구현 완료된 모든 기능:

📬 기본 기능

  • IMAP 프로토콜 메일 수신
  • 메일 목록 표시
  • 읽음/안읽음 상태
  • 첨부파일 유무 표시

📄 상세보기

  • HTML 본문 렌더링
  • 텍스트 본문 보기
  • 자동 읽음 처리
  • 첨부파일 다운로드

🔍 고급 기능

  • 통합 검색 (제목/발신자/내용)
  • 필터링 (읽음/첨부파일)
  • 정렬 (날짜/발신자)
  • 자동 새로고침 (30초)

🔒 보안

  • XSS 방지 (DOMPurify)
  • 비밀번호 암호화
  • 안전한 파일명 생성
); }