[RAPID-micro] 메일관리 계정 추가 버튼을 계정 목록 패널로 이동
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user