From a085b8149d8701b2faae3a7a6e5c5399cd8a9bea Mon Sep 17 00:00:00 2001 From: syc0123 Date: Wed, 1 Apr 2026 15:11:11 +0900 Subject: [PATCH] =?UTF-8?q?[RAPID-micro]=20=EB=A9=94=EC=9D=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EB=82=B4=EB=8A=94=20=EC=82=AC=EB=9E=8C=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EC=84=A0=ED=83=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../src/services/userMailImapService.ts | 23 +++++++- .../app/(main)/mail/imap/ComposeDialog.tsx | 57 +++++++++++++++---- frontend/app/(main)/mail/imap/page.tsx | 37 +++++++++--- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/backend-node/src/services/userMailImapService.ts b/backend-node/src/services/userMailImapService.ts index a9d483e0..c135aa2c 100644 --- a/backend-node/src/services/userMailImapService.ts +++ b/backend-node/src/services/userMailImapService.ts @@ -390,7 +390,28 @@ class UserMailImapService { await client.logout(); return { success: true, message: 'IMAP 연결 성공' }; } catch (err) { - return { success: false, message: err instanceof Error ? err.message : '연결 실패' }; + let message = '연결 실패'; + if (err instanceof Error) { + const imapErr = err as any; + const raw = imapErr.response || imapErr.responseCode || imapErr.cause?.message || err.message; + const r = String(raw).toLowerCase(); + if (r.includes('authentication') || r.includes('invalid credentials') || r.includes('authenticationfailed') || r.includes('login failed')) { + message = '인증 실패: 이메일 주소 또는 비밀번호가 올바르지 않습니다.'; + } else if (r.includes('econnrefused') || r.includes('connection refused')) { + message = '연결 거부: 호스트 또는 포트를 확인하세요.'; + } else if (r.includes('enotfound') || r.includes('getaddrinfo')) { + message = '호스트를 찾을 수 없습니다. IMAP 주소를 확인하세요.'; + } else if (r.includes('timeout') || r.includes('etimedout')) { + message = '연결 시간 초과: 서버가 응답하지 않습니다.'; + } else if (r.includes('self signed') || r.includes('certificate')) { + message = 'SSL 인증서 오류가 발생했습니다.'; + } else if (r.includes('econnreset')) { + message = '연결이 강제로 끊겼습니다. TLS/SSL 설정을 확인하세요.'; + } else { + message = raw; + } + } + return { success: false, message }; } } } diff --git a/frontend/app/(main)/mail/imap/ComposeDialog.tsx b/frontend/app/(main)/mail/imap/ComposeDialog.tsx index 3de386f4..296d7960 100644 --- a/frontend/app/(main)/mail/imap/ComposeDialog.tsx +++ b/frontend/app/(main)/mail/imap/ComposeDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import LinkExtension from "@tiptap/extension-link"; @@ -15,7 +15,15 @@ import { DialogTitle, DialogFooter, } from "@/components/ui/dialog"; -import { sendUserMail } from "@/lib/api/userMail"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "sonner"; +import { sendUserMail, UserMailAccount } from "@/lib/api/userMail"; interface ComposeDialogProps { open: boolean; @@ -34,6 +42,7 @@ interface ComposeDialogProps { sending: boolean; setSending: (v: boolean) => void; accountId: number | null; + accounts: UserMailAccount[]; } export default function ComposeDialog({ @@ -43,8 +52,14 @@ export default function ComposeDialog({ initialHtml, setInitialHtml, inReplyTo, references, sending, setSending, - accountId, + accountId, accounts, }: ComposeDialogProps) { + const [fromAccountId, setFromAccountId] = useState(accountId); + + useEffect(() => { + setFromAccountId(accountId ?? (accounts[0]?.id ?? null)); + }, [accountId, accounts, open]); + const editor = useEditor({ extensions: [StarterKit, LinkExtension.configure({ openOnClick: false })], content: initialHtml, @@ -58,11 +73,11 @@ export default function ComposeDialog({ }, [initialHtml, editor]); async function handleSend() { - if (!accountId || !editor) return; + if (!fromAccountId || !editor) return; setSending(true); try { const html = editor.getHTML(); - const result = await sendUserMail(accountId, { + const result = await sendUserMail(fromAccountId, { to, cc: cc || undefined, subject, @@ -74,7 +89,7 @@ export default function ComposeDialog({ onOpenChange(false); setTo(""); setCc(""); setSubject(""); setInitialHtml(""); } else { - alert(result.message); + toast.error(result.message); } } finally { setSending(false); @@ -89,27 +104,45 @@ export default function ComposeDialog({ {mode === "reply" ? "답장" : mode === "forward" ? "전달" : "새 메일"} -
-
+
+
+ + +
+
setTo(e.target.value)} placeholder="to@example.com" />
-
+
setCc(e.target.value)} placeholder="cc@example.com (선택)" />
-
+
setSubject(e.target.value)} />
-
+
- diff --git a/frontend/app/(main)/mail/imap/page.tsx b/frontend/app/(main)/mail/imap/page.tsx index e67a5c94..ccdeb317 100644 --- a/frontend/app/(main)/mail/imap/page.tsx +++ b/frontend/app/(main)/mail/imap/page.tsx @@ -87,7 +87,8 @@ import { SendMailDto, } from "@/lib/api/userMail"; -const ComposeDialogDynamic = dynamic(() => import("./ComposeDialog"), { ssr: false }); +import type ComposeDialogType from "./ComposeDialog"; +const ComposeDialogDynamic = dynamic(() => import("./ComposeDialog"), { ssr: false }) as typeof ComposeDialogType; const IMAP_PRESETS = [ { label: "직접 입력", value: "custom", host: "", port: 993, useTls: true }, @@ -128,6 +129,7 @@ export default function ImapMailPage() { const [searchTerm, setSearchTerm] = useState(""); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); + const [formErrors, setFormErrors] = useState>>({}); // New states const [folders, setFolders] = useState([]); @@ -431,6 +433,7 @@ export default function ImapMailPage() { setTestResult(null); setShowPassword(false); setHostPopoverOpen(false); + setFormErrors({}); setShowDialog(true); } @@ -460,6 +463,14 @@ export default function ImapMailPage() { } async function handleSave() { + const errors: typeof formErrors = {}; + if (!form.displayName.trim()) errors.displayName = "표시 이름을 입력하세요."; + if (!form.email.trim()) errors.email = "이메일 주소를 입력하세요."; + if (!form.host.trim()) errors.host = "IMAP 호스트를 입력하세요."; + if (!form.username.trim()) errors.username = "사용자명을 입력하세요."; + if (!editingAccount && !form.password.trim()) errors.password = "비밀번호를 입력하세요."; + if (Object.keys(errors).length > 0) { setFormErrors(errors); return; } + setFormErrors({}); setSaving(true); setSaveError(null); try { @@ -897,6 +908,7 @@ export default function ImapMailPage() { references={composeReferences} sending={composeSending} setSending={setComposeSending} accountId={selectedAccount?.id ?? null} + accounts={imapAccounts} /> {/* 계정 추가/편집 다이얼로그 */} @@ -912,18 +924,22 @@ export default function ImapMailPage() { setForm((p) => ({ ...p, displayName: e.target.value }))} + onChange={(e) => { setForm((p) => ({ ...p, displayName: e.target.value })); setFormErrors((p) => ({ ...p, displayName: undefined })); }} placeholder="내 Gmail" + className={formErrors.displayName ? "border-destructive" : ""} /> + {formErrors.displayName &&

{formErrors.displayName}

}
setForm((p) => ({ ...p, email: e.target.value, username: e.target.value }))} + onChange={(e) => { setForm((p) => ({ ...p, email: e.target.value, username: e.target.value })); setFormErrors((p) => ({ ...p, email: undefined })); }} placeholder="user@example.com" + className={formErrors.email ? "border-destructive" : ""} /> + {formErrors.email &&

{formErrors.email}

}
@@ -937,12 +953,12 @@ export default function ImapMailPage() { - + setForm((p) => ({ ...p, host: v }))} + onValueChange={(v) => { setForm((p) => ({ ...p, host: v })); setFormErrors((p) => ({ ...p, host: undefined })); }} /> 직접 입력한 값을 사용합니다 @@ -968,6 +984,7 @@ export default function ImapMailPage() { + {formErrors.host &&

{formErrors.host}

}
@@ -986,9 +1003,11 @@ export default function ImapMailPage() { setForm((p) => ({ ...p, username: e.target.value }))} + onChange={(e) => { setForm((p) => ({ ...p, username: e.target.value })); setFormErrors((p) => ({ ...p, username: undefined })); }} placeholder="user@example.com" + className={formErrors.username ? "border-destructive" : ""} /> + {formErrors.username &&

{formErrors.username}

}
@@ -996,9 +1015,10 @@ export default function ImapMailPage() { setForm((p) => ({ ...p, password: e.target.value }))} + onChange={(e) => { setForm((p) => ({ ...p, password: e.target.value })); setFormErrors((p) => ({ ...p, password: undefined })); }} + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} placeholder="••••••••" - className="pr-9" + className={`pr-9 ${formErrors.password ? "border-destructive" : ""}`} />
+ {formErrors.password &&

{formErrors.password}

}
{editingAccount && (