메일관리 기능 구현

This commit is contained in:
leeheejin 2025-10-02 15:46:23 +09:00
parent 3fa410cbe4
commit bf58e0c878
6 changed files with 109 additions and 13 deletions

View File

@ -178,14 +178,19 @@ export class MailAccountFileController {
try { try {
const { id } = req.params; const { id } = req.params;
// TODO: 실제 SMTP 연결 테스트 구현 const account = await mailAccountFileService.getAccountById(id);
// const account = await mailAccountFileService.getAccountById(id); if (!account) {
// nodemailer로 연결 테스트 return res.status(404).json({
success: false,
message: '계정을 찾을 수 없습니다.',
});
}
return res.json({ // mailSendSimpleService의 testConnection 사용
success: true, const { mailSendSimpleService } = require('../services/mailSendSimpleService');
message: '연결 테스트 성공 (미구현)', const result = await mailSendSimpleService.testConnection(id);
});
return res.json(result);
} catch (error: unknown) { } catch (error: unknown) {
const err = error as Error; const err = error as Error;
return res.status(500).json({ return res.status(500).json({

View File

@ -7,10 +7,12 @@ export class MailSendSimpleController {
*/ */
async sendMail(req: Request, res: Response) { async sendMail(req: Request, res: Response) {
try { try {
console.log('📧 메일 발송 요청 수신:', { accountId: req.body.accountId, to: req.body.to, subject: req.body.subject });
const { accountId, templateId, to, subject, variables, customHtml } = req.body; const { accountId, templateId, to, subject, variables, customHtml } = req.body;
// 필수 파라미터 검증 // 필수 파라미터 검증
if (!accountId || !to || !Array.isArray(to) || to.length === 0) { if (!accountId || !to || !Array.isArray(to) || to.length === 0) {
console.log('❌ 필수 파라미터 누락');
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '계정 ID와 수신자 이메일이 필요합니다.', message: '계정 ID와 수신자 이메일이 필요합니다.',

View File

@ -6,6 +6,7 @@
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { mailAccountFileService } from './mailAccountFileService'; import { mailAccountFileService } from './mailAccountFileService';
import { mailTemplateFileService } from './mailTemplateFileService'; import { mailTemplateFileService } from './mailTemplateFileService';
import { encryptionService } from './encryptionService';
export interface SendMailRequest { export interface SendMailRequest {
accountId: string; accountId: string;
@ -56,18 +57,39 @@ class MailSendSimpleService {
throw new Error('메일 내용이 없습니다.'); throw new Error('메일 내용이 없습니다.');
} }
// 4. SMTP 연결 생성 // 4. 비밀번호 복호화
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
console.log('🔐 비밀번호 복호화 완료');
console.log('🔐 암호화된 비밀번호 (일부):', account.smtpPassword.substring(0, 30) + '...');
console.log('🔐 복호화된 비밀번호 길이:', decryptedPassword.length);
// 5. SMTP 연결 생성
// 포트 465는 SSL/TLS를 사용해야 함
const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
console.log('📧 SMTP 연결 설정:', {
host: account.smtpHost,
port: account.smtpPort,
secure: isSecure,
user: account.smtpUsername,
});
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: account.smtpHost, host: account.smtpHost,
port: account.smtpPort, port: account.smtpPort,
secure: account.smtpSecure, // SSL/TLS secure: isSecure, // SSL/TLS (포트 465는 자동으로 true)
auth: { auth: {
user: account.smtpUsername, user: account.smtpUsername,
pass: account.smtpPassword, pass: decryptedPassword, // 복호화된 비밀번호 사용
}, },
// 타임아웃 설정 (30초)
connectionTimeout: 30000,
greetingTimeout: 30000,
}); });
// 5. 메일 발송 console.log('📧 메일 발송 시도 중...');
// 6. 메일 발송
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: `"${account.name}" <${account.email}>`, from: `"${account.name}" <${account.email}>`,
to: request.to.join(', '), to: request.to.join(', '),
@ -75,6 +97,12 @@ class MailSendSimpleService {
html: htmlContent, html: htmlContent,
}); });
console.log('✅ 메일 발송 성공:', {
messageId: info.messageId,
accepted: info.accepted,
rejected: info.rejected,
});
return { return {
success: true, success: true,
messageId: info.messageId, messageId: info.messageId,
@ -83,6 +111,8 @@ class MailSendSimpleService {
}; };
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
console.error('❌ 메일 발송 실패:', err.message);
console.error('❌ 에러 상세:', err);
return { return {
success: false, success: false,
error: err.message, error: err.message,
@ -178,22 +208,42 @@ class MailSendSimpleService {
*/ */
async testConnection(accountId: string): Promise<{ success: boolean; message: string }> { async testConnection(accountId: string): Promise<{ success: boolean; message: string }> {
try { try {
console.log('🔌 SMTP 연결 테스트 시작:', accountId);
const account = await mailAccountFileService.getAccountById(accountId); const account = await mailAccountFileService.getAccountById(accountId);
if (!account) { if (!account) {
throw new Error('계정을 찾을 수 없습니다.'); throw new Error('계정을 찾을 수 없습니다.');
} }
// 비밀번호 복호화
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
console.log('🔐 비밀번호 복호화 완료');
// 포트 465는 SSL/TLS를 사용해야 함
const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
console.log('🔌 SMTP 연결 설정:', {
host: account.smtpHost,
port: account.smtpPort,
secure: isSecure,
user: account.smtpUsername,
});
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: account.smtpHost, host: account.smtpHost,
port: account.smtpPort, port: account.smtpPort,
secure: account.smtpSecure, secure: isSecure,
auth: { auth: {
user: account.smtpUsername, user: account.smtpUsername,
pass: account.smtpPassword, pass: decryptedPassword, // 복호화된 비밀번호 사용
}, },
connectionTimeout: 10000, // 10초 타임아웃
greetingTimeout: 10000,
}); });
console.log('🔌 SMTP 연결 검증 중...');
await transporter.verify(); await transporter.verify();
console.log('✅ SMTP 연결 검증 성공!');
return { return {
success: true, success: true,
@ -201,6 +251,7 @@ class MailSendSimpleService {
}; };
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
console.error('❌ SMTP 연결 실패:', err.message);
return { return {
success: false, success: false,
message: `연결 실패: ${err.message}`, message: `연결 실패: ${err.message}`,

View File

@ -10,6 +10,7 @@ import {
createMailAccount, createMailAccount,
updateMailAccount, updateMailAccount,
deleteMailAccount, deleteMailAccount,
testMailAccountConnection,
CreateMailAccountDto, CreateMailAccountDto,
UpdateMailAccountDto, UpdateMailAccountDto,
} from "@/lib/api/mail"; } from "@/lib/api/mail";
@ -104,6 +105,24 @@ export default function MailAccountsPage() {
} }
}; };
const handleTestConnection = async (account: MailAccount) => {
try {
setLoading(true);
const result = await testMailAccountConnection(account.id);
if (result.success) {
alert(`✅ SMTP 연결 성공!\n\n${result.message || '정상적으로 연결되었습니다.'}`);
} else {
alert(`❌ SMTP 연결 실패\n\n${result.message || '연결에 실패했습니다.'}`);
}
} catch (error: any) {
console.error('연결 테스트 실패:', error);
alert(`❌ SMTP 연결 테스트 실패\n\n${error.message || '알 수 없는 오류가 발생했습니다.'}`);
} finally {
setLoading(false);
}
};
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="w-full max-w-none px-4 py-8 space-y-8"> <div className="w-full max-w-none px-4 py-8 space-y-8">
@ -148,6 +167,7 @@ export default function MailAccountsPage() {
onEdit={handleOpenEditModal} onEdit={handleOpenEditModal}
onDelete={handleOpenDeleteModal} onDelete={handleOpenDeleteModal}
onToggleStatus={handleToggleStatus} onToggleStatus={handleToggleStatus}
onTestConnection={handleTestConnection}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -21,6 +21,7 @@ interface MailAccountTableProps {
onEdit: (account: MailAccount) => void; onEdit: (account: MailAccount) => void;
onDelete: (account: MailAccount) => void; onDelete: (account: MailAccount) => void;
onToggleStatus: (account: MailAccount) => void; onToggleStatus: (account: MailAccount) => void;
onTestConnection: (account: MailAccount) => void;
} }
export default function MailAccountTable({ export default function MailAccountTable({
@ -28,6 +29,7 @@ export default function MailAccountTable({
onEdit, onEdit,
onDelete, onDelete,
onToggleStatus, onToggleStatus,
onTestConnection,
}: MailAccountTableProps) { }: MailAccountTableProps) {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState<keyof MailAccount>('createdAt'); const [sortField, setSortField] = useState<keyof MailAccount>('createdAt');
@ -220,6 +222,13 @@ export default function MailAccountTable({
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<button
onClick={() => onTestConnection(account)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="SMTP 연결 테스트"
>
<Zap className="w-4 h-4" />
</button>
<button <button
onClick={() => onEdit(account)} onClick={() => onEdit(account)}
className="p-2 text-primary hover:bg-accent rounded-lg transition-colors" className="p-2 text-primary hover:bg-accent rounded-lg transition-colors"

View File

@ -168,6 +168,15 @@ export async function deleteMailAccount(id: string): Promise<{ success: boolean
}); });
} }
/**
* SMTP
*/
export async function testMailAccountConnection(id: string): Promise<{ success: boolean; message: string }> {
return fetchApi<{ success: boolean; message: string }>(`/accounts/${id}/test-connection`, {
method: 'POST',
});
}
/** /**
* SMTP * SMTP
*/ */