feat: add frontend/src/components/MailList.tsx
This commit is contained in:
parent
e72fcd0ab1
commit
3cd4ff055a
107
frontend/src/components/MailList.tsx
Normal file
107
frontend/src/components/MailList.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useMailStore } from '../store/mailStore'
|
||||
import { mailApi } from '../api/mailApi'
|
||||
import { formatDistanceToNow, parseISO } from 'date-fns'
|
||||
import { ko } from 'date-fns/locale'
|
||||
|
||||
function fmtDate(d: string) {
|
||||
try {
|
||||
return formatDistanceToNow(parseISO(d), { addSuffix: true, locale: ko })
|
||||
} catch {
|
||||
return d?.slice(0, 16) || ''
|
||||
}
|
||||
}
|
||||
|
||||
function fmtSize(b: number) {
|
||||
if (b < 1024) return `${b}B`
|
||||
if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)}KB`
|
||||
return `${(b / 1024 / 1024).toFixed(1)}MB`
|
||||
}
|
||||
|
||||
interface Props { onRefresh: () => void }
|
||||
|
||||
export default function MailList({ onRefresh }: Props) {
|
||||
const { messages, loading, currentFolder, totalMessages, currentPage,
|
||||
selectedMail, setSelectedMail, setLoading, setMessages,
|
||||
markRead, removeMessage } = useMailStore()
|
||||
|
||||
// Sent 폴더에서는 받는사람 표시
|
||||
const isSent = currentFolder === 'Sent'
|
||||
|
||||
const select = async (uid: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const detail = await mailApi.detail(uid, currentFolder)
|
||||
setSelectedMail(detail)
|
||||
markRead(uid)
|
||||
} catch { /* 오류 무시 */ }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const del = async (e: React.MouseEvent, uid: string) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await mailApi.delete(uid, currentFolder)
|
||||
removeMessage(uid)
|
||||
} catch { /* 오류 무시 */ }
|
||||
}
|
||||
|
||||
const perPage = 50
|
||||
const totalPages = Math.ceil(totalMessages / perPage)
|
||||
|
||||
const goPage = async (p: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await mailApi.messages(currentFolder, p)
|
||||
setMessages(data.messages || [], data.total || 0, p)
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
|
||||
if (loading) return <div className="mail-list-loading">⏳ 불러오는 중...</div>
|
||||
if (!messages.length) return (
|
||||
<div className="mail-list-empty">
|
||||
<div className="empty-icon">📭</div>
|
||||
<div>메일이 없습니다</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mail-list">
|
||||
<div className="mail-list-header">
|
||||
<span className="mail-count">{totalMessages}개</span>
|
||||
<button className="btn-icon" onClick={onRefresh} title="새로고침">🔄</button>
|
||||
</div>
|
||||
|
||||
<ul className="mail-items">
|
||||
{messages.map(m => (
|
||||
<li
|
||||
key={m.uid}
|
||||
className={`mail-item ${!m.is_read ? 'unread' : ''} ${selectedMail?.uid === m.uid ? 'selected' : ''}`}
|
||||
onClick={() => select(m.uid)}
|
||||
>
|
||||
<div className="mail-item-top">
|
||||
<span className="mail-sender">
|
||||
{isSent ? `→ ${(m as any).to || m.sender_addr}` : (m.sender || m.sender_addr)}
|
||||
</span>
|
||||
<span className="mail-date">{fmtDate(m.date)}</span>
|
||||
</div>
|
||||
<div className="mail-item-subject">{m.subject}</div>
|
||||
<div className="mail-item-preview">
|
||||
{m.has_attachment && <span title="첨부파일">📎 </span>}
|
||||
{m.preview}
|
||||
<span className="mail-size">{fmtSize(m.size)}</span>
|
||||
</div>
|
||||
<button className="btn-delete" onClick={e => del(e, m.uid)} title="삭제">🗑</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="pagination">
|
||||
<button disabled={currentPage <= 1} onClick={() => goPage(currentPage - 1)}>◀</button>
|
||||
<span>{currentPage} / {totalPages}</span>
|
||||
<button disabled={currentPage >= totalPages} onClick={() => goPage(currentPage + 1)}>▶</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user