[RAPID-micro] 메일관리 계정 추가 버튼을 계정 목록 패널로 이동

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 14:18:49 +09:00
parent 1b92c283fb
commit 8ddb7319ab

View File

@@ -13,6 +13,17 @@ import {
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";
import { Switch } from "@/components/ui/switch";
import {
ResizablePanelGroup,
@@ -44,6 +55,8 @@ import {
FolderOpen,
Send,
Download,
Eye,
EyeOff,
} from "lucide-react";
import DOMPurify from "isomorphic-dompurify";
import {
@@ -115,6 +128,9 @@ export default function ImapMailPage() {
const [composeInReplyTo, setComposeInReplyTo] = useState("");
const [composeReferences, setComposeReferences] = useState("");
const [composeSending, setComposeSending] = useState(false);
const [pendingDeleteMail, setPendingDeleteMail] = useState<ReceivedMail | null>(null);
const [pendingDeleteAccount, setPendingDeleteAccount] = useState<UserMailAccount | null>(null);
const [showPassword, setShowPassword] = useState(false);
const detailCacheRef = useRef<Map<string, MailDetail>>(new Map());
const prefetchingRef = useRef<Set<string>>(new Set());
@@ -321,7 +337,13 @@ export default function ImapMailPage() {
async function handleDeleteMail(mail: ReceivedMail) {
if (!selectedAccount) return;
if (!confirm("메일을 삭제하시겠습니까?")) return;
setPendingDeleteMail(mail);
}
async function confirmDeleteMail() {
if (!selectedAccount || !pendingDeleteMail) return;
const mail = pendingDeleteMail;
setPendingDeleteMail(null);
const seqno = parseInt(mail.id.split("-").pop() || "0");
try {
await deleteUserMail(selectedAccount.id, seqno);
@@ -334,7 +356,7 @@ export default function ImapMailPage() {
if (selectedMail?.id === mail.id) setSelectedMail(null);
loadFolders(selectedAccount);
} catch (e: any) {
alert("메일 삭제 실패: " + e.message);
toast.error("메일 삭제 실패: " + e.message);
}
}
@@ -393,6 +415,7 @@ export default function ImapMailPage() {
setEditingAccount(null);
setForm(DEFAULT_FORM);
setTestResult(null);
setShowPassword(false);
setShowDialog(true);
}
@@ -409,6 +432,7 @@ export default function ImapMailPage() {
password: "",
});
setTestResult(null);
setShowPassword(false);
setShowDialog(true);
}
@@ -440,7 +464,13 @@ export default function ImapMailPage() {
}
async function handleDeleteAccount(account: UserMailAccount) {
if (!confirm(`"${account.displayName}" 계정을 삭제하시겠습니까?`)) return;
setPendingDeleteAccount(account);
}
async function confirmDeleteAccount() {
if (!pendingDeleteAccount) return;
const account = pendingDeleteAccount;
setPendingDeleteAccount(null);
try {
await deleteUserMailAccount(account.id);
if (selectedAccount?.id === account.id) {
@@ -511,10 +541,6 @@ export default function ImapMailPage() {
<Plus className="h-4 w-4 mr-1" />
</Button>
<Button size="sm" onClick={openAddDialog}>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
@@ -527,7 +553,14 @@ export default function ImapMailPage() {
<div className="px-3 py-2 border-b text-xs font-medium text-muted-foreground flex items-center gap-1">
<Inbox className="h-3.5 w-3.5" />
{loadingAccounts && <Loader2 className="h-3 w-3 animate-spin ml-auto" />}
{loadingAccounts && <Loader2 className="h-3 w-3 animate-spin" />}
<button
className="ml-auto p-0.5 hover:text-foreground text-muted-foreground"
onClick={openAddDialog}
title="계정 추가"
>
<Plus className="h-3.5 w-3.5" />
</button>
</div>
<div className="flex-1 overflow-y-auto">
{imapAccounts.length === 0 && !loadingAccounts ? (
@@ -633,7 +666,7 @@ export default function ImapMailPage() {
) : filteredMails.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-2">
<Inbox className="h-8 w-8 opacity-30" />
<p className="text-sm"> </p>
<p className="text-sm">{searchTerm ? `"${searchTerm}" 검색 결과가 없습니다` : "메일이 없습니다"}</p>
</div>
) : (
<>
@@ -756,13 +789,13 @@ export default function ImapMailPage() {
try {
const list = await getUserMailAttachments(accountId, seqno, currentFolder);
const matched = list.find(a => a.filename === att.filename) || list[i];
if (!matched) { alert('첨부파일 정보를 불러올 수 없습니다'); return; }
if (!matched) { toast.error('첨부파일 정보를 불러올 수 없습니다'); return; }
await downloadAttachment(
accountId, seqno, matched.partId, matched.filename, currentFolder,
(pct) => setDownloadProgress(p => ({ ...p, [i]: pct })),
matched.size
);
} catch (e: any) { alert(e.message); }
} catch (e: any) { toast.error(e.message); }
finally { setDownloadProgress(p => { const next = { ...p }; delete next[i]; return next; }); }
}}
className="relative inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-secondary hover:bg-accent border disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer min-w-[80px] overflow-hidden">
@@ -873,7 +906,7 @@ export default function ImapMailPage() {
<Input
type="email"
value={form.email}
onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))}
onChange={(e) => setForm((p) => ({ ...p, email: e.target.value, username: e.target.value }))}
placeholder="user@example.com"
/>
</div>
@@ -909,12 +942,22 @@ export default function ImapMailPage() {
</div>
<div className="space-y-1">
<Label className="text-xs"> {editingAccount && "(변경 시에만 입력)"}</Label>
<Input
type="password"
value={form.password}
onChange={(e) => setForm((p) => ({ ...p, password: e.target.value }))}
placeholder="••••••••"
/>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
value={form.password}
onChange={(e) => setForm((p) => ({ ...p, password: e.target.value }))}
placeholder="••••••••"
className="pr-9"
/>
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setShowPassword((v) => !v)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
{editingAccount && (
<div>
@@ -958,6 +1001,34 @@ export default function ImapMailPage() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* 메일 삭제 확인 */}
<AlertDialog open={!!pendingDeleteMail} onOpenChange={(open) => { if (!open) setPendingDeleteMail(null); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> ?</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDeleteMail} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 계정 삭제 확인 */}
<AlertDialog open={!!pendingDeleteAccount} onOpenChange={(open) => { if (!open) setPendingDeleteAccount(null); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>"{pendingDeleteAccount?.displayName}" ?</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDeleteAccount} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}