메일관리 기능 구현 완료

This commit is contained in:
leeheejin
2025-10-13 15:17:34 +09:00
parent b4c5be1f17
commit 95c98cbda3
40 changed files with 2227 additions and 4150 deletions

View File

@@ -73,8 +73,8 @@ app.use(
})
);
app.use(compression());
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
// 정적 파일 서빙 (업로드된 파일들)
app.use(
@@ -165,6 +165,17 @@ app.use("/api/layouts", layoutRoutes);
app.use("/api/mail/accounts", mailAccountFileRoutes); // 파일 기반 계정
app.use("/api/mail/templates-file", mailTemplateFileRoutes); // 파일 기반 템플릿
app.use("/api/mail/send", mailSendSimpleRoutes); // 메일 발송
// 메일 수신 라우트 디버깅 - 모든 요청 로깅
app.use("/api/mail/receive", (req, res, next) => {
console.log(`\n🔍 [MAIL RECEIVE REQUEST]`);
console.log(` Method: ${req.method}`);
console.log(` URL: ${req.originalUrl}`);
console.log(` Path: ${req.path}`);
console.log(` Base URL: ${req.baseUrl}`);
console.log(` Params: ${JSON.stringify(req.params)}`);
console.log(` Query: ${JSON.stringify(req.query)}`);
next();
});
app.use("/api/mail/receive", mailReceiveBasicRoutes); // 메일 수신
app.use("/api/mail/sent", mailSentHistoryRoutes); // 발송 이력
app.use("/api/screen", screenStandardRoutes);

View File

@@ -18,6 +18,12 @@ export class MailReceiveBasicController {
*/
async getMailList(req: Request, res: Response) {
try {
console.log('📬 메일 목록 조회 요청:', {
params: req.params,
path: req.path,
originalUrl: req.originalUrl
});
const { accountId } = req.params;
const limit = parseInt(req.query.limit as string) || 50;
@@ -43,6 +49,12 @@ export class MailReceiveBasicController {
*/
async getMailDetail(req: Request, res: Response) {
try {
console.log('🔍 메일 상세 조회 요청:', {
params: req.params,
path: req.path,
originalUrl: req.originalUrl
});
const { accountId, seqno } = req.params;
const seqnoNumber = parseInt(seqno, 10);
@@ -109,29 +121,39 @@ export class MailReceiveBasicController {
*/
async downloadAttachment(req: Request, res: Response) {
try {
console.log('📎🎯 컨트롤러 downloadAttachment 진입');
const { accountId, seqno, index } = req.params;
console.log(`📎 파라미터: accountId=${accountId}, seqno=${seqno}, index=${index}`);
const seqnoNumber = parseInt(seqno, 10);
const indexNumber = parseInt(index, 10);
if (isNaN(seqnoNumber) || isNaN(indexNumber)) {
console.log('❌ 유효하지 않은 파라미터');
return res.status(400).json({
success: false,
message: '유효하지 않은 파라미터입니다.',
});
}
console.log('📎 서비스 호출 시작...');
const result = await this.mailReceiveService.downloadAttachment(
accountId,
seqnoNumber,
indexNumber
);
console.log(`📎 서비스 호출 완료: result=${result ? '있음' : '없음'}`);
if (!result) {
console.log('❌ 첨부파일을 찾을 수 없음');
return res.status(404).json({
success: false,
message: '첨부파일을 찾을 수 없습니다.',
});
}
console.log(`📎 파일 다운로드 시작: ${result.filename}`);
console.log(`📎 파일 경로: ${result.filePath}`);
// 파일 다운로드
res.download(result.filePath, result.filename, (err) => {
@@ -173,5 +195,27 @@ export class MailReceiveBasicController {
});
}
}
/**
* GET /api/mail/receive/today-count
* 오늘 수신 메일 수 조회
*/
async getTodayReceivedCount(req: Request, res: Response) {
try {
const { accountId } = req.query;
const count = await this.mailReceiveService.getTodayReceivedCount(accountId as string);
return res.json({
success: true,
data: { count }
});
} catch (error: unknown) {
console.error('오늘 수신 메일 수 조회 실패:', error);
return res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '오늘 수신 메일 수 조회에 실패했습니다.'
});
}
}
}

View File

@@ -19,6 +19,9 @@ export class MailSendSimpleController {
// FormData에서 JSON 문자열 파싱
const accountId = req.body.accountId;
const templateId = req.body.templateId;
const modifiedTemplateComponents = req.body.modifiedTemplateComponents
? JSON.parse(req.body.modifiedTemplateComponents)
: undefined; // 🎯 수정된 템플릿 컴포넌트
const to = req.body.to ? JSON.parse(req.body.to) : [];
const cc = req.body.cc ? JSON.parse(req.body.cc) : undefined;
const bcc = req.body.bcc ? JSON.parse(req.body.bcc) : undefined;
@@ -90,6 +93,7 @@ export class MailSendSimpleController {
const result = await mailSendSimpleService.sendMail({
accountId,
templateId,
modifiedTemplateComponents, // 🎯 수정된 템플릿 컴포넌트 전달
to,
cc,
bcc,

View File

@@ -12,20 +12,29 @@ const router = express.Router();
router.use(authenticateToken);
const controller = new MailReceiveBasicController();
// 메일 목록 조회
router.get('/:accountId', (req, res) => controller.getMailList(req, res));
// 오늘 수신 메일 조회 (통계) - 가장 먼저 정의 (가장 구체적)
router.get('/today-count', (req, res) => controller.getTodayReceivedCount(req, res));
// 메일 상세 조회
router.get('/:accountId/:seqno', (req, res) => controller.getMailDetail(req, res));
// 첨부파일 다운로드 - 매우 구체적인 경로
router.get('/:accountId/:seqno/attachment/:index', (req, res) => {
console.log(`📎 첨부파일 라우트 핸들러 진입!`);
console.log(` accountId: ${req.params.accountId}`);
console.log(` seqno: ${req.params.seqno}`);
console.log(` index: ${req.params.index}`);
controller.downloadAttachment(req, res);
});
// 첨부파일 다운로드 (상세 조회보다 먼저 정의해야 함)
router.get('/:accountId/:seqno/attachment/:index', (req, res) => controller.downloadAttachment(req, res));
// 메일 읽음 표시
// 메일 읽음 표시 - 구체적인 경로
router.post('/:accountId/:seqno/mark-read', (req, res) => controller.markAsRead(req, res));
// IMAP 연결 테스트
// 메일 상세 조회 - /:accountId보다 먼저 정의해야 함
router.get('/:accountId/:seqno', (req, res) => controller.getMailDetail(req, res));
// IMAP 연결 테스트 - /:accountId보다 먼저 정의해야 함
router.post('/:accountId/test-imap', (req, res) => controller.testImapConnection(req, res));
// 메일 목록 조회 - 가장 마지막에 정의 (가장 일반적)
router.get('/:accountId', (req, res) => controller.getMailList(req, res));
export default router;

View File

@@ -109,7 +109,7 @@ export class MailReceiveBasicService {
tls: true,
};
console.log(`📧 IMAP 연결 시도 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, 이메일: ${imapConfig.user}`);
// console.log(`📧 IMAP 연결 시도 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, 이메일: ${imapConfig.user}`);
return new Promise((resolve, reject) => {
const imap = this.createImapConnection(imapConfig);
@@ -117,26 +117,26 @@ export class MailReceiveBasicService {
// 30초 타임아웃 설정
const timeout = setTimeout(() => {
console.error('❌ IMAP 연결 타임아웃 (30초)');
// console.error('❌ IMAP 연결 타임아웃 (30초)');
imap.end();
reject(new Error('IMAP 연결 타임아웃'));
}, 30000);
imap.once('ready', () => {
console.log('✅ IMAP 연결 성공! INBOX 열기 시도...');
// console.log('✅ IMAP 연결 성공! INBOX 열기 시도...');
clearTimeout(timeout);
imap.openBox('INBOX', true, (err: any, box: any) => {
if (err) {
console.error('❌ INBOX 열기 실패:', err);
// console.error('❌ INBOX 열기 실패:', err);
imap.end();
return reject(err);
}
console.log(`📬 INBOX 열림 - 전체 메일 수: ${box.messages.total}`);
// console.log(`📬 INBOX 열림 - 전체 메일 수: ${box.messages.total}`);
const totalMessages = box.messages.total;
if (totalMessages === 0) {
console.log('📭 메일함이 비어있습니다');
// console.log('📭 메일함이 비어있습니다');
imap.end();
return resolve([]);
}
@@ -145,19 +145,19 @@ export class MailReceiveBasicService {
const start = Math.max(1, totalMessages - limit + 1);
const end = totalMessages;
console.log(`📨 메일 가져오기 시작 - 범위: ${start}~${end}`);
// console.log(`📨 메일 가져오기 시작 - 범위: ${start}~${end}`);
const fetch = imap.seq.fetch(`${start}:${end}`, {
bodies: ['HEADER', 'TEXT'],
struct: true,
});
console.log(`📦 fetch 객체 생성 완료`);
// console.log(`📦 fetch 객체 생성 완료`);
let processedCount = 0;
const totalToProcess = end - start + 1;
fetch.on('message', (msg: any, seqno: any) => {
console.log(`📬 메일 #${seqno} 처리 시작`);
// console.log(`📬 메일 #${seqno} 처리 시작`);
let header: string = '';
let body: string = '';
let attributes: any = null;
@@ -207,10 +207,10 @@ export class MailReceiveBasicService {
};
mails.push(mail);
console.log(`✓ 메일 #${seqno} 파싱 완료 (${mails.length}/${totalToProcess})`);
// console.log(`✓ 메일 #${seqno} 파싱 완료 (${mails.length}/${totalToProcess})`);
processedCount++;
} catch (parseError) {
console.error(`메일 #${seqno} 파싱 오류:`, parseError);
// console.error(`메일 #${seqno} 파싱 오류:`, parseError);
processedCount++;
}
}
@@ -219,24 +219,24 @@ export class MailReceiveBasicService {
});
fetch.once('error', (fetchErr: any) => {
console.error('❌ 메일 fetch 에러:', fetchErr);
// console.error('❌ 메일 fetch 에러:', fetchErr);
imap.end();
reject(fetchErr);
});
fetch.once('end', () => {
console.log(`📭 fetch 종료 - 처리 완료 대기 중... (현재: ${mails.length}개)`);
// console.log(`📭 fetch 종료 - 처리 완료 대기 중... (현재: ${mails.length}개)`);
// 모든 메일 처리가 완료될 때까지 대기
const checkComplete = setInterval(() => {
console.log(`⏳ 대기 중 - 처리됨: ${processedCount}/${totalToProcess}, 메일: ${mails.length}`);
// console.log(`⏳ 대기 중 - 처리됨: ${processedCount}/${totalToProcess}, 메일: ${mails.length}개`);
if (processedCount >= totalToProcess) {
clearInterval(checkComplete);
console.log(`✅ 메일 가져오기 완료 - 총 ${mails.length}`);
// console.log(`✅ 메일 가져오기 완료 - 총 ${mails.length}개`);
imap.end();
// 최신 메일이 위로 오도록 정렬
mails.sort((a, b) => b.date.getTime() - a.date.getTime());
console.log(`📤 메일 목록 반환: ${mails.length}`);
// console.log(`📤 메일 목록 반환: ${mails.length}개`);
resolve(mails);
}
}, 100);
@@ -244,7 +244,7 @@ export class MailReceiveBasicService {
// 최대 10초 대기
setTimeout(() => {
clearInterval(checkComplete);
console.log(`⚠️ 타임아웃 - 부분 반환: ${mails.length}/${totalToProcess}`);
// console.log(`⚠️ 타임아웃 - 부분 반환: ${mails.length}/${totalToProcess}개`);
imap.end();
mails.sort((a, b) => b.date.getTime() - a.date.getTime());
resolve(mails);
@@ -254,16 +254,16 @@ export class MailReceiveBasicService {
});
imap.once('error', (imapErr: any) => {
console.error('❌ IMAP 연결 에러:', imapErr.message || imapErr);
// console.error('❌ IMAP 연결 에러:', imapErr.message || imapErr);
clearTimeout(timeout);
reject(imapErr);
});
imap.once('end', () => {
console.log('🔌 IMAP 연결 종료');
// console.log('🔌 IMAP 연결 종료');
});
console.log('🔗 IMAP.connect() 호출...');
// console.log('🔗 IMAP.connect() 호출...');
imap.connect();
});
}
@@ -311,22 +311,36 @@ export class MailReceiveBasicService {
return reject(err);
}
console.log(`📬 INBOX 정보 - 전체 메일: ${box.messages.total}, 요청한 seqno: ${seqno}`);
if (seqno > box.messages.total || seqno < 1) {
console.error(`❌ 유효하지 않은 seqno: ${seqno} (메일 총 개수: ${box.messages.total})`);
imap.end();
return resolve(null);
}
const fetch = imap.seq.fetch(`${seqno}:${seqno}`, {
bodies: '',
struct: true,
});
let mailDetail: MailDetail | null = null;
let parsingComplete = false;
fetch.on('message', (msg: any, seqnum: any) => {
console.log(`📨 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
msg.on('body', (stream: any, info: any) => {
console.log(`📝 메일 본문 스트림 시작 - which: ${info.which}`);
let buffer = '';
stream.on('data', (chunk: any) => {
buffer += chunk.toString('utf8');
});
stream.once('end', async () => {
console.log(`✅ 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`);
try {
const parsed = await simpleParser(buffer);
console.log(`✅ 메일 파싱 완료 - 제목: ${parsed.subject}`);
const fromAddress = Array.isArray(parsed.from) ? parsed.from[0] : parsed.from;
const toAddress = Array.isArray(parsed.to) ? parsed.to[0] : parsed.to;
@@ -353,21 +367,48 @@ export class MailReceiveBasicService {
size: att.size || 0,
})),
};
parsingComplete = true;
} catch (parseError) {
console.error('메일 파싱 오류:', parseError);
parsingComplete = true;
}
});
});
// msg 전체가 처리되었을 때 이벤트
msg.once('end', () => {
console.log(`📮 메일 메시지 처리 완료 - seqnum: ${seqnum}`);
});
});
fetch.once('error', (fetchErr: any) => {
console.error(`❌ Fetch 에러:`, fetchErr);
imap.end();
reject(fetchErr);
});
fetch.once('end', () => {
imap.end();
resolve(mailDetail);
console.log(`🏁 Fetch 종료 - parsingComplete: ${parsingComplete}`);
// 비동기 파싱이 완료될 때까지 대기
const waitForParsing = setInterval(() => {
if (parsingComplete) {
clearInterval(waitForParsing);
console.log(`✅ 파싱 완료 대기 종료 - mailDetail이 ${mailDetail ? '존재함' : 'null'}`);
imap.end();
resolve(mailDetail);
}
}, 10); // 10ms마다 체크
// 타임아웃 설정 (10초)
setTimeout(() => {
if (!parsingComplete) {
clearInterval(waitForParsing);
console.error('❌ 파싱 타임아웃');
imap.end();
resolve(mailDetail); // 타임아웃 시에도 현재 상태 반환
}
}, 10000);
});
});
});
@@ -492,6 +533,43 @@ export class MailReceiveBasicService {
}
}
/**
* 오늘 수신한 메일 수 조회 (통계용)
*/
async getTodayReceivedCount(accountId?: string): Promise<number> {
try {
const accounts = accountId
? [await mailAccountFileService.getAccountById(accountId)]
: await mailAccountFileService.getAllAccounts();
const today = new Date();
today.setHours(0, 0, 0, 0);
let totalCount = 0;
for (const account of accounts) {
if (!account) continue;
try {
const mails = await this.fetchMailList(account.id, 100);
const todayMails = mails.filter(mail => {
const mailDate = new Date(mail.date);
return mailDate >= today;
});
totalCount += todayMails.length;
} catch (error) {
// 개별 계정 오류는 무시하고 계속 진행
console.error(`계정 ${account.id} 메일 조회 실패:`, error);
}
}
return totalCount;
} catch (error) {
console.error('오늘 수신 메일 수 조회 실패:', error);
return 0;
}
}
/**
* 첨부파일 다운로드
*/
@@ -533,19 +611,26 @@ export class MailReceiveBasicService {
});
let attachmentResult: { filePath: string; filename: string; contentType: string } | null = null;
let parsingComplete = false;
fetch.on('message', (msg: any, seqnum: any) => {
console.log(`📎 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
msg.on('body', (stream: any, info: any) => {
console.log(`📎 메일 본문 스트림 시작`);
let buffer = '';
stream.on('data', (chunk: any) => {
buffer += chunk.toString('utf8');
});
stream.once('end', async () => {
console.log(`📎 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`);
try {
const parsed = await simpleParser(buffer);
console.log(`📎 파싱 완료 - 첨부파일 개수: ${parsed.attachments?.length || 0}`);
if (parsed.attachments && parsed.attachments[attachmentIndex]) {
const attachment = parsed.attachments[attachmentIndex];
console.log(`📎 첨부파일 발견 (index ${attachmentIndex}): ${attachment.filename}`);
// 안전한 파일명 생성
const safeFilename = this.sanitizeFilename(
@@ -557,28 +642,51 @@ export class MailReceiveBasicService {
// 파일 저장
await fs.writeFile(filePath, attachment.content);
console.log(`📎 파일 저장 완료: ${filePath}`);
attachmentResult = {
filePath,
filename: attachment.filename || 'unnamed',
contentType: attachment.contentType || 'application/octet-stream',
};
parsingComplete = true;
} else {
console.log(`❌ 첨부파일 index ${attachmentIndex}를 찾을 수 없음 (총 ${parsed.attachments?.length || 0}개)`);
parsingComplete = true;
}
} catch (parseError) {
console.error('첨부파일 파싱 오류:', parseError);
parsingComplete = true;
}
});
});
});
fetch.once('error', (fetchErr: any) => {
console.error('❌ fetch 오류:', fetchErr);
imap.end();
reject(fetchErr);
});
fetch.once('end', () => {
imap.end();
resolve(attachmentResult);
console.log('📎 fetch.once("end") 호출됨 - 파싱 완료 대기 시작...');
// 파싱 완료를 기다림 (최대 5초)
const checkComplete = setInterval(() => {
if (parsingComplete) {
console.log(`✅ 파싱 완료 확인 - attachmentResult: ${attachmentResult ? '있음' : '없음'}`);
clearInterval(checkComplete);
imap.end();
resolve(attachmentResult);
}
}, 100);
setTimeout(() => {
clearInterval(checkComplete);
console.log(`⚠️ 타임아웃 - attachmentResult: ${attachmentResult ? '있음' : '없음'}`);
imap.end();
resolve(attachmentResult);
}, 5000);
});
});
});

View File

@@ -12,6 +12,7 @@ import { mailSentHistoryService } from './mailSentHistoryService';
export interface SendMailRequest {
accountId: string;
templateId?: string;
modifiedTemplateComponents?: any[]; // 🎯 프론트엔드에서 수정된 템플릿 컴포넌트
to: string[]; // 받는 사람
cc?: string[]; // 참조 (Carbon Copy)
bcc?: string[]; // 숨은참조 (Blind Carbon Copy)
@@ -52,15 +53,29 @@ class MailSendSimpleService {
throw new Error('비활성 상태의 계정입니다.');
}
// 3. HTML 생성 (템플릿 또는 커스텀)
htmlContent = request.customHtml || '';
if (!htmlContent && request.templateId) {
// 3. HTML 생성 (템플릿 + 추가 메시지 병합)
if (request.templateId) {
// 템플릿 사용
const template = await mailTemplateFileService.getTemplateById(request.templateId);
if (!template) {
throw new Error('템플릿을 찾을 수 없습니다.');
}
// 🎯 수정된 컴포넌트가 있으면 덮어쓰기
if (request.modifiedTemplateComponents && request.modifiedTemplateComponents.length > 0) {
console.log('✏️ 수정된 템플릿 컴포넌트 사용:', request.modifiedTemplateComponents.length);
template.components = request.modifiedTemplateComponents;
}
htmlContent = this.renderTemplate(template, request.variables);
// 템플릿 + 추가 메시지 병합
if (request.customHtml && request.customHtml.trim()) {
htmlContent = this.mergeTemplateAndCustomContent(htmlContent, request.customHtml);
}
} else {
// 직접 작성
htmlContent = request.customHtml || '';
}
if (!htmlContent) {
@@ -261,13 +276,25 @@ class MailSendSimpleService {
}
/**
* 템플릿 렌더링 (간단 버전)
* 템플릿 렌더링 (일반 메일 양식)
*/
private renderTemplate(
template: any,
variables?: Record<string, string>
): string {
let html = '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
// 일반적인 메일 레이아웃 (전체 너비, 그림자 없음)
let html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #ffffff;">
<tr>
<td style="padding: 20px;">
`;
template.components.forEach((component: any) => {
switch (component.type) {
@@ -276,20 +303,23 @@ class MailSendSimpleService {
if (variables) {
content = this.replaceVariables(content, variables);
}
html += `<p style="margin: 16px 0; color: ${component.color || '#333'}; font-size: ${component.fontSize || '14px'};">${content}</p>`;
// 텍스트는 왼쪽 정렬, 적절한 줄간격
html += `<div style="margin: 0 0 20px 0; color: ${component.color || '#333'}; font-size: ${component.fontSize || '15px'}; line-height: 1.6; text-align: left;">${content}</div>`;
break;
case 'button':
let buttonText = component.text || 'Button';
if (variables) {
buttonText = this.replaceVariables(buttonText, variables);
}
html += `<div style="text-align: center; margin: 24px 0;">
<a href="${component.url || '#'}" style="display: inline-block; padding: 12px 24px; background-color: ${component.backgroundColor || '#007bff'}; color: ${component.textColor || '#fff'}; text-decoration: none; border-radius: 4px;">${buttonText}</a>
// 버튼은 왼쪽 정렬 (text-align 제거)
html += `<div style="margin: 30px 0; text-align: left;">
<a href="${component.url || '#'}" style="display: inline-block; padding: 14px 28px; background-color: ${component.backgroundColor || '#007bff'}; color: ${component.textColor || '#fff'}; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 15px;">${buttonText}</a>
</div>`;
break;
case 'image':
html += `<div style="text-align: center; margin: 16px 0;">
<img src="${component.src}" alt="${component.alt || ''}" style="max-width: 100%; height: auto;" />
// 이미지는 왼쪽 정렬
html += `<div style="margin: 20px 0; text-align: left;">
<img src="${component.src}" alt="${component.alt || ''}" style="max-width: 100%; height: auto; display: block; border-radius: 4px;" />
</div>`;
break;
case 'spacer':
@@ -298,7 +328,13 @@ class MailSendSimpleService {
}
});
html += '</div>';
html += `
</td>
</tr>
</table>
</body>
</html>
`;
return html;
}
@@ -320,6 +356,52 @@ class MailSendSimpleService {
return result;
}
/**
* 템플릿과 추가 메시지 병합
* 템플릿 HTML의 body 태그 끝 부분에 추가 메시지를 삽입
*/
private mergeTemplateAndCustomContent(templateHtml: string, customContent: string): string {
// customContent에 HTML 태그가 없으면 기본 스타일 적용
let formattedCustomContent = customContent;
if (!customContent.includes('<')) {
// 일반 텍스트인 경우 단락으로 변환
const paragraphs = customContent
.split('\n\n')
.filter((p) => p.trim())
.map((p) => `<p style="margin: 16px 0; line-height: 1.6;">${p.replace(/\n/g, '<br>')}</p>`)
.join('');
formattedCustomContent = `
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb;">
${paragraphs}
</div>
`;
} else {
// 이미 HTML인 경우 구분선만 추가
formattedCustomContent = `
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb;">
${customContent}
</div>
`;
}
// </body> 또는 </div> 태그 앞에 삽입
if (templateHtml.includes('</body>')) {
return templateHtml.replace('</body>', `${formattedCustomContent}</body>`);
} else if (templateHtml.includes('</div>')) {
// 마지막 </div> 앞에 삽입
const lastDivIndex = templateHtml.lastIndexOf('</div>');
return (
templateHtml.substring(0, lastDivIndex) +
formattedCustomContent +
templateHtml.substring(lastDivIndex)
);
} else {
// 태그가 없으면 단순 결합
return templateHtml + formattedCustomContent;
}
}
/**
* SMTP 연결 테스트
*/

View File

@@ -141,26 +141,35 @@ class MailTemplateFileService {
id: string,
data: Partial<Omit<MailTemplate, 'id' | 'createdAt'>>
): Promise<MailTemplate | null> {
const existing = await this.getTemplateById(id);
if (!existing) {
return null;
try {
const existing = await this.getTemplateById(id);
if (!existing) {
// console.error(`❌ 템플릿을 찾을 수 없음: ${id}`);
return null;
}
const updated: MailTemplate = {
...existing,
...data,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
// console.log(`📝 템플릿 저장 시도: ${id}, 크기: ${JSON.stringify(updated).length} bytes`);
await fs.writeFile(
this.getTemplatePath(id),
JSON.stringify(updated, null, 2),
'utf-8'
);
// console.log(`✅ 템플릿 저장 성공: ${id}`);
return updated;
} catch (error) {
// console.error(`❌ 템플릿 저장 실패: ${id}`, error);
throw error; // 에러를 컨트롤러로 전달
}
const updated: MailTemplate = {
...existing,
...data,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
await fs.writeFile(
this.getTemplatePath(id),
JSON.stringify(updated, null, 2),
'utf-8'
);
return updated;
}
/**