메일관리
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "1bb5ebfe-3f6c-4884-a043-161ae3f74f75",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Fwd: ㄴㅇㄹㅇㄴㄴㄹ 테스트트트",
|
||||
"htmlContent": "\n\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n전달된 메일:\n\n보낸사람: \"이희진\" <zian9227@naver.com>\n날짜: 2025. 10. 22. 오후 4:24:54\n제목: ㄴㅇㄹㅇㄴㄴㄹ\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nㄹㅇㄴㄹㅇㄴㄹㅇㄴ\n",
|
||||
"sentAt": "2025-10-22T07:49:50.811Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:49:50.811Z",
|
||||
"deletedAt": "2025-10-22T07:50:14.211Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "1d997eeb-3d61-427d-8b54-119d4372b9b3",
|
||||
"sentAt": "2025-10-22T07:13:30.905Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"subject": "Fwd: ㄴ",
|
||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">전달히야야양</p><p style=\"margin: 0 0 16px 0; line-height: 1.6;\"><br>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<br>전달된 메일:</p><p style=\"margin: 0 0 16px 0; line-height: 1.6;\">보낸사람: \"이희진\" <zian9227@naver.com><br>날짜: 2025. 10. 22. 오후 12:58:15<br>제목: ㄴ<br>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</p><p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹ<br></p>\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<d20cd501-04a4-bbe6-8b50-7f43e19bd70a@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "331d95d6-3a13-4657-bc75-ab0811712eb8",
|
||||
"sentAt": "2025-10-22T07:18:18.240Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"subject": "ㅁㄴㅇㄹㅁㄴㅇㄹ",
|
||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹㅁㄴㅇㄹㄴㅇㄹㄴㅇㄹ</p>\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<d4923c0d-f692-7d1d-d1b0-3b9e1e6cbab5@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -14,5 +14,6 @@
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
"rejected": [],
|
||||
"deletedAt": "2025-10-22T07:11:04.666Z"
|
||||
}
|
||||
@@ -11,5 +11,6 @@
|
||||
"sentAt": "2025-10-22T07:04:27.192Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:04:57.280Z"
|
||||
"updatedAt": "2025-10-22T07:04:57.280Z",
|
||||
"deletedAt": "2025-10-22T07:50:17.136Z"
|
||||
}
|
||||
@@ -13,5 +13,6 @@
|
||||
"sentAt": "2025-10-22T06:56:51.060Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:56:51.060Z"
|
||||
"updatedAt": "2025-10-22T06:56:51.060Z",
|
||||
"deletedAt": "2025-10-22T07:50:22.989Z"
|
||||
}
|
||||
@@ -11,5 +11,6 @@
|
||||
"sentAt": "2025-10-22T06:57:53.335Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:00:23.394Z"
|
||||
"updatedAt": "2025-10-22T07:00:23.394Z",
|
||||
"deletedAt": "2025-10-22T07:50:20.510Z"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "4a32bab5-364e-4037-bb00-31d2905824db",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "테스트 마지가",
|
||||
"htmlContent": "ㅁㄴㅇㄹ",
|
||||
"sentAt": "2025-10-22T07:49:29.948Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:49:29.948Z",
|
||||
"deletedAt": "2025-10-22T07:50:12.374Z"
|
||||
}
|
||||
@@ -11,5 +11,6 @@
|
||||
"sentAt": "2025-10-22T07:03:09.080Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:03:39.150Z"
|
||||
"updatedAt": "2025-10-22T07:03:39.150Z",
|
||||
"deletedAt": "2025-10-22T07:50:19.035Z"
|
||||
}
|
||||
@@ -13,5 +13,6 @@
|
||||
"sentAt": "2025-10-22T06:54:55.097Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:54:55.097Z"
|
||||
"updatedAt": "2025-10-22T06:54:55.097Z",
|
||||
"deletedAt": "2025-10-22T07:50:24.672Z"
|
||||
}
|
||||
@@ -11,5 +11,6 @@
|
||||
"sentAt": "2025-10-22T06:41:52.984Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:46:23.051Z"
|
||||
"updatedAt": "2025-10-22T06:46:23.051Z",
|
||||
"deletedAt": "2025-10-22T07:50:29.124Z"
|
||||
}
|
||||
@@ -8,5 +8,6 @@
|
||||
"sentAt": "2025-10-22T06:17:31.379Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:17:31.379Z"
|
||||
"updatedAt": "2025-10-22T06:17:31.379Z",
|
||||
"deletedAt": "2025-10-22T07:50:30.736Z"
|
||||
}
|
||||
@@ -14,5 +14,6 @@
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
"rejected": [],
|
||||
"deletedAt": "2025-10-22T07:11:10.245Z"
|
||||
}
|
||||
@@ -13,5 +13,6 @@
|
||||
"sentAt": "2025-10-22T06:50:04.224Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:50:04.224Z"
|
||||
"updatedAt": "2025-10-22T06:50:04.224Z",
|
||||
"deletedAt": "2025-10-22T07:50:26.224Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "a638f7d0-ee31-47fa-9f72-de66ef31ea44",
|
||||
"sentAt": "2025-10-22T07:21:13.723Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"subject": "ㄹㅇㄴㅁㄹㅇㄴㅁ",
|
||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㄹㅇㄴㅁㄹㅇㄴㅁㅇㄹㅇㄴㅁ</p>\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<5ea07d02-78bf-a655-8289-bcbd8eaf7741@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -13,5 +13,6 @@
|
||||
"sentAt": "2025-10-22T06:47:53.815Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:48:53.876Z"
|
||||
"updatedAt": "2025-10-22T06:48:53.876Z",
|
||||
"deletedAt": "2025-10-22T07:50:27.706Z"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "cf892a77-1998-4165-bb9d-b390451465b2",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Fwd: ㄴ",
|
||||
"htmlContent": "\n\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n전달된 메일:\n\n보낸사람: \"이희진\" <zian9227@naver.com>\n날짜: 2025. 10. 22. 오후 12:58:15\n제목: ㄴ\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n",
|
||||
"sentAt": "2025-10-22T07:06:11.620Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:07:11.749Z",
|
||||
"deletedAt": "2025-10-22T07:50:15.739Z"
|
||||
}
|
||||
@@ -8,5 +8,6 @@
|
||||
"sentAt": "2025-10-22T06:15:02.128Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:15:02.128Z"
|
||||
"updatedAt": "2025-10-22T06:15:02.128Z",
|
||||
"deletedAt": "2025-10-22T07:08:43.543Z"
|
||||
}
|
||||
@@ -23,5 +23,6 @@
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
"rejected": [],
|
||||
"deletedAt": "2025-10-22T07:11:12.907Z"
|
||||
}
|
||||
@@ -18,11 +18,11 @@ export class MailReceiveBasicController {
|
||||
*/
|
||||
async getMailList(req: Request, res: Response) {
|
||||
try {
|
||||
console.log('📬 메일 목록 조회 요청:', {
|
||||
params: req.params,
|
||||
path: req.path,
|
||||
originalUrl: req.originalUrl
|
||||
});
|
||||
// console.log('📬 메일 목록 조회 요청:', {
|
||||
// params: req.params,
|
||||
// path: req.path,
|
||||
// originalUrl: req.originalUrl
|
||||
// });
|
||||
|
||||
const { accountId } = req.params;
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
@@ -49,11 +49,11 @@ export class MailReceiveBasicController {
|
||||
*/
|
||||
async getMailDetail(req: Request, res: Response) {
|
||||
try {
|
||||
console.log('🔍 메일 상세 조회 요청:', {
|
||||
params: req.params,
|
||||
path: req.path,
|
||||
originalUrl: req.originalUrl
|
||||
});
|
||||
// console.log('🔍 메일 상세 조회 요청:', {
|
||||
// params: req.params,
|
||||
// path: req.path,
|
||||
// originalUrl: req.originalUrl
|
||||
// });
|
||||
|
||||
const { accountId, seqno } = req.params;
|
||||
const seqnoNumber = parseInt(seqno, 10);
|
||||
@@ -121,39 +121,39 @@ export class MailReceiveBasicController {
|
||||
*/
|
||||
async downloadAttachment(req: Request, res: Response) {
|
||||
try {
|
||||
console.log('📎🎯 컨트롤러 downloadAttachment 진입');
|
||||
// console.log('📎🎯 컨트롤러 downloadAttachment 진입');
|
||||
const { accountId, seqno, index } = req.params;
|
||||
console.log(`📎 파라미터: accountId=${accountId}, seqno=${seqno}, index=${index}`);
|
||||
// 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('❌ 유효하지 않은 파라미터');
|
||||
// console.log('❌ 유효하지 않은 파라미터');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 파라미터입니다.',
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📎 서비스 호출 시작...');
|
||||
// console.log('📎 서비스 호출 시작...');
|
||||
const result = await this.mailReceiveService.downloadAttachment(
|
||||
accountId,
|
||||
seqnoNumber,
|
||||
indexNumber
|
||||
);
|
||||
console.log(`📎 서비스 호출 완료: result=${result ? '있음' : '없음'}`);
|
||||
// console.log(`📎 서비스 호출 완료: result=${result ? '있음' : '없음'}`);
|
||||
|
||||
if (!result) {
|
||||
console.log('❌ 첨부파일을 찾을 수 없음');
|
||||
// console.log('❌ 첨부파일을 찾을 수 없음');
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '첨부파일을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📎 파일 다운로드 시작: ${result.filename}`);
|
||||
console.log(`📎 파일 경로: ${result.filePath}`);
|
||||
// console.log(`📎 파일 다운로드 시작: ${result.filename}`);
|
||||
// console.log(`📎 파일 경로: ${result.filePath}`);
|
||||
|
||||
// 파일 다운로드
|
||||
res.download(result.filePath, result.filename, (err) => {
|
||||
@@ -247,3 +247,5 @@ export class MailReceiveBasicController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const mailReceiveBasicController = new MailReceiveBasicController();
|
||||
|
||||
@@ -7,14 +7,14 @@ export class MailSendSimpleController {
|
||||
*/
|
||||
async sendMail(req: Request, res: Response) {
|
||||
try {
|
||||
console.log('📧 메일 발송 요청 수신:', {
|
||||
accountId: req.body.accountId,
|
||||
to: req.body.to,
|
||||
cc: req.body.cc,
|
||||
bcc: req.body.bcc,
|
||||
subject: req.body.subject,
|
||||
attachments: req.files ? (req.files as Express.Multer.File[]).length : 0,
|
||||
});
|
||||
// console.log('📧 메일 발송 요청 수신:', {
|
||||
// accountId: req.body.accountId,
|
||||
// to: req.body.to,
|
||||
// cc: req.body.cc,
|
||||
// bcc: req.body.bcc,
|
||||
// subject: req.body.subject,
|
||||
// attachments: req.files ? (req.files as Express.Multer.File[]).length : 0,
|
||||
// });
|
||||
|
||||
// FormData에서 JSON 문자열 파싱
|
||||
const accountId = req.body.accountId;
|
||||
@@ -31,7 +31,7 @@ export class MailSendSimpleController {
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if (!accountId || !to || !Array.isArray(to) || to.length === 0) {
|
||||
console.log('❌ 필수 파라미터 누락');
|
||||
// console.log('❌ 필수 파라미터 누락');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '계정 ID와 수신자 이메일이 필요합니다.',
|
||||
@@ -63,9 +63,9 @@ export class MailSendSimpleController {
|
||||
if (req.body.fileNames) {
|
||||
try {
|
||||
parsedFileNames = JSON.parse(req.body.fileNames);
|
||||
console.log('📎 프론트엔드에서 받은 파일명들:', parsedFileNames);
|
||||
// console.log('📎 프론트엔드에서 받은 파일명들:', parsedFileNames);
|
||||
} catch (e) {
|
||||
console.warn('파일명 파싱 실패, multer originalname 사용');
|
||||
// console.warn('파일명 파싱 실패, multer originalname 사용');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,10 @@ export class MailSendSimpleController {
|
||||
});
|
||||
});
|
||||
|
||||
console.log('📎 최종 첨부파일 정보:', attachments.map(a => ({
|
||||
filename: a.filename,
|
||||
path: a.path.split('/').pop()
|
||||
})));
|
||||
// console.log('📎 최종 첨부파일 정보:', attachments.map(a => ({
|
||||
// filename: a.filename,
|
||||
// path: a.path.split('/').pop()
|
||||
// })));
|
||||
}
|
||||
|
||||
// 메일 발송
|
||||
@@ -130,16 +130,24 @@ export class MailSendSimpleController {
|
||||
*/
|
||||
async sendBulkMail(req: Request, res: Response) {
|
||||
try {
|
||||
const { accountId, templateId, subject, recipients } = req.body;
|
||||
const { accountId, templateId, customHtml, subject, recipients } = req.body;
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if (!accountId || !templateId || !subject || !recipients || !Array.isArray(recipients)) {
|
||||
if (!accountId || !subject || !recipients || !Array.isArray(recipients)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 파라미터가 누락되었습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// 템플릿 또는 직접 작성 중 하나는 있어야 함
|
||||
if (!templateId && !customHtml) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '템플릿 또는 메일 내용 중 하나는 필수입니다.',
|
||||
});
|
||||
}
|
||||
|
||||
if (recipients.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -147,12 +155,13 @@ export class MailSendSimpleController {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📧 대량 발송 요청: ${recipients.length}명`);
|
||||
// console.log(`📧 대량 발송 요청: ${recipients.length}명`);
|
||||
|
||||
// 대량 발송 실행
|
||||
const result = await mailSendSimpleService.sendBulkMail({
|
||||
accountId,
|
||||
templateId,
|
||||
templateId, // 선택
|
||||
customHtml, // 선택
|
||||
subject,
|
||||
recipients,
|
||||
});
|
||||
|
||||
@@ -119,7 +119,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);
|
||||
@@ -133,7 +133,7 @@ export class MailReceiveBasicService {
|
||||
}, 30000);
|
||||
|
||||
imap.once("ready", () => {
|
||||
// console.log('✅ IMAP 연결 성공! INBOX 열기 시도...');
|
||||
// // console.log('✅ IMAP 연결 성공! INBOX 열기 시도...');
|
||||
clearTimeout(timeout);
|
||||
|
||||
imap.openBox("INBOX", true, (err: any, box: any) => {
|
||||
@@ -143,10 +143,10 @@ export class MailReceiveBasicService {
|
||||
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([]);
|
||||
}
|
||||
@@ -155,19 +155,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;
|
||||
@@ -225,7 +225,7 @@ 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);
|
||||
@@ -243,18 +243,18 @@ export class MailReceiveBasicService {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -262,7 +262,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);
|
||||
@@ -278,10 +278,10 @@ export class MailReceiveBasicService {
|
||||
});
|
||||
|
||||
imap.once("end", () => {
|
||||
// console.log('🔌 IMAP 연결 종료');
|
||||
// // console.log('🔌 IMAP 연결 종료');
|
||||
});
|
||||
|
||||
// console.log('🔗 IMAP.connect() 호출...');
|
||||
// // console.log('🔗 IMAP.connect() 호출...');
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
@@ -332,9 +332,9 @@ export class MailReceiveBasicService {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`📬 INBOX 정보 - 전체 메일: ${box.messages.total}, 요청한 seqno: ${seqno}`
|
||||
);
|
||||
// console.log(
|
||||
// `📬 INBOX 정보 - 전체 메일: ${box.messages.total}, 요청한 seqno: ${seqno}`
|
||||
// );
|
||||
|
||||
if (seqno > box.messages.total || seqno < 1) {
|
||||
console.error(
|
||||
@@ -353,21 +353,21 @@ export class MailReceiveBasicService {
|
||||
let parsingComplete = false;
|
||||
|
||||
fetch.on("message", (msg: any, seqnum: any) => {
|
||||
console.log(`📨 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
|
||||
// console.log(`📨 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
|
||||
|
||||
msg.on("body", (stream: any, info: any) => {
|
||||
console.log(`📝 메일 본문 스트림 시작 - which: ${info.which}`);
|
||||
// console.log(`📝 메일 본문 스트림 시작 - which: ${info.which}`);
|
||||
let buffer = "";
|
||||
stream.on("data", (chunk: any) => {
|
||||
buffer += chunk.toString("utf8");
|
||||
});
|
||||
stream.once("end", async () => {
|
||||
console.log(
|
||||
`✅ 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`
|
||||
);
|
||||
// console.log(
|
||||
// `✅ 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`
|
||||
// );
|
||||
try {
|
||||
const parsed = await simpleParser(buffer);
|
||||
console.log(`✅ 메일 파싱 완료 - 제목: ${parsed.subject}`);
|
||||
// console.log(`✅ 메일 파싱 완료 - 제목: ${parsed.subject}`);
|
||||
|
||||
const fromAddress = Array.isArray(parsed.from)
|
||||
? parsed.from[0]
|
||||
@@ -415,7 +415,7 @@ export class MailReceiveBasicService {
|
||||
|
||||
// msg 전체가 처리되었을 때 이벤트
|
||||
msg.once("end", () => {
|
||||
console.log(`📮 메일 메시지 처리 완료 - seqnum: ${seqnum}`);
|
||||
// console.log(`📮 메일 메시지 처리 완료 - seqnum: ${seqnum}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -426,15 +426,15 @@ export class MailReceiveBasicService {
|
||||
});
|
||||
|
||||
fetch.once("end", () => {
|
||||
console.log(`🏁 Fetch 종료 - parsingComplete: ${parsingComplete}`);
|
||||
// console.log(`🏁 Fetch 종료 - parsingComplete: ${parsingComplete}`);
|
||||
|
||||
// 비동기 파싱이 완료될 때까지 대기
|
||||
const waitForParsing = setInterval(() => {
|
||||
if (parsingComplete) {
|
||||
clearInterval(waitForParsing);
|
||||
console.log(
|
||||
`✅ 파싱 완료 대기 종료 - mailDetail이 ${mailDetail ? "존재함" : "null"}`
|
||||
);
|
||||
// console.log(
|
||||
// `✅ 파싱 완료 대기 종료 - mailDetail이 ${mailDetail ? "존재함" : "null"}`
|
||||
// );
|
||||
imap.end();
|
||||
resolve(mailDetail);
|
||||
}
|
||||
@@ -499,7 +499,7 @@ export class MailReceiveBasicService {
|
||||
|
||||
imap.once("ready", () => {
|
||||
clearTimeout(timeout);
|
||||
console.log(`🔗 IMAP 연결 성공 - 읽음 표시 시작 (seqno=${seqno})`);
|
||||
// console.log(`🔗 IMAP 연결 성공 - 읽음 표시 시작 (seqno=${seqno})`);
|
||||
|
||||
// false로 변경: 쓰기 가능 모드로 INBOX 열기
|
||||
imap.openBox("INBOX", false, (err: any, box: any) => {
|
||||
@@ -509,7 +509,7 @@ export class MailReceiveBasicService {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
console.log(`📬 INBOX 열림 (쓰기 가능 모드)`);
|
||||
// console.log(`📬 INBOX 열림 (쓰기 가능 모드)`);
|
||||
|
||||
imap.seq.addFlags(seqno, ["\\Seen"], (flagErr: any) => {
|
||||
imap.end();
|
||||
@@ -517,7 +517,7 @@ export class MailReceiveBasicService {
|
||||
console.error("❌ 읽음 플래그 설정 실패:", flagErr);
|
||||
reject(flagErr);
|
||||
} else {
|
||||
console.log("✅ 읽음 플래그 설정 성공 - seqno:", seqno);
|
||||
// console.log("✅ 읽음 플래그 설정 성공 - seqno:", seqno);
|
||||
resolve({
|
||||
success: true,
|
||||
message: "메일을 읽음으로 표시했습니다.",
|
||||
@@ -537,7 +537,7 @@ export class MailReceiveBasicService {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
|
||||
console.log(`🔌 IMAP 연결 시도 중... (host=${imapConfig.host}, port=${imapConfig.port})`);
|
||||
// console.log(`🔌 IMAP 연결 시도 중... (host=${imapConfig.host}, port=${imapConfig.port})`);
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
@@ -556,7 +556,7 @@ export class MailReceiveBasicService {
|
||||
|
||||
// 비밀번호 복호화
|
||||
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
||||
// console.log(`🔐 IMAP 테스트 - 이메일: ${account.email}, 비밀번호 길이: ${decryptedPassword.length}`);
|
||||
// // console.log(`🔐 IMAP 테스트 - 이메일: ${account.email}, 비밀번호 길이: ${decryptedPassword.length}`);
|
||||
|
||||
const accountAny = account as any;
|
||||
const imapConfig: ImapConfig = {
|
||||
@@ -566,7 +566,7 @@ export class MailReceiveBasicService {
|
||||
port: this.inferImapPort(account.smtpPort, accountAny.imapPort),
|
||||
tls: true,
|
||||
};
|
||||
// console.log(`📧 IMAP 설정 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, TLS: ${imapConfig.tls}`);
|
||||
// // console.log(`📧 IMAP 설정 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, TLS: ${imapConfig.tls}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imap = this.createImapConnection(imapConfig);
|
||||
@@ -692,32 +692,32 @@ export class MailReceiveBasicService {
|
||||
let parsingComplete = false;
|
||||
|
||||
fetch.on("message", (msg: any, seqnum: any) => {
|
||||
console.log(`📎 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
|
||||
// console.log(`📎 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
|
||||
|
||||
msg.on("body", (stream: any, info: any) => {
|
||||
console.log(`📎 메일 본문 스트림 시작`);
|
||||
// console.log(`📎 메일 본문 스트림 시작`);
|
||||
let buffer = "";
|
||||
stream.on("data", (chunk: any) => {
|
||||
buffer += chunk.toString("utf8");
|
||||
});
|
||||
stream.once("end", async () => {
|
||||
console.log(
|
||||
`📎 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`
|
||||
);
|
||||
// console.log(
|
||||
// `📎 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`
|
||||
// );
|
||||
try {
|
||||
const parsed = await simpleParser(buffer);
|
||||
console.log(
|
||||
`📎 파싱 완료 - 첨부파일 개수: ${parsed.attachments?.length || 0}`
|
||||
);
|
||||
// console.log(
|
||||
// `📎 파싱 완료 - 첨부파일 개수: ${parsed.attachments?.length || 0}`
|
||||
// );
|
||||
|
||||
if (
|
||||
parsed.attachments &&
|
||||
parsed.attachments[attachmentIndex]
|
||||
) {
|
||||
const attachment = parsed.attachments[attachmentIndex];
|
||||
console.log(
|
||||
`📎 첨부파일 발견 (index ${attachmentIndex}): ${attachment.filename}`
|
||||
);
|
||||
// console.log(
|
||||
// `📎 첨부파일 발견 (index ${attachmentIndex}): ${attachment.filename}`
|
||||
// );
|
||||
|
||||
// 안전한 파일명 생성
|
||||
const safeFilename = this.sanitizeFilename(
|
||||
@@ -729,7 +729,7 @@ export class MailReceiveBasicService {
|
||||
|
||||
// 파일 저장
|
||||
await fs.writeFile(filePath, attachment.content);
|
||||
console.log(`📎 파일 저장 완료: ${filePath}`);
|
||||
// console.log(`📎 파일 저장 완료: ${filePath}`);
|
||||
|
||||
attachmentResult = {
|
||||
filePath,
|
||||
@@ -739,9 +739,9 @@ export class MailReceiveBasicService {
|
||||
};
|
||||
parsingComplete = true;
|
||||
} else {
|
||||
console.log(
|
||||
`❌ 첨부파일 index ${attachmentIndex}를 찾을 수 없음 (총 ${parsed.attachments?.length || 0}개)`
|
||||
);
|
||||
// console.log(
|
||||
// `❌ 첨부파일 index ${attachmentIndex}를 찾을 수 없음 (총 ${parsed.attachments?.length || 0}개)`
|
||||
// );
|
||||
parsingComplete = true;
|
||||
}
|
||||
} catch (parseError) {
|
||||
@@ -759,14 +759,14 @@ export class MailReceiveBasicService {
|
||||
});
|
||||
|
||||
fetch.once("end", () => {
|
||||
console.log('📎 fetch.once("end") 호출됨 - 파싱 완료 대기 시작...');
|
||||
// console.log('📎 fetch.once("end") 호출됨 - 파싱 완료 대기 시작...');
|
||||
|
||||
// 파싱 완료를 기다림 (최대 5초)
|
||||
const checkComplete = setInterval(() => {
|
||||
if (parsingComplete) {
|
||||
console.log(
|
||||
`✅ 파싱 완료 확인 - attachmentResult: ${attachmentResult ? "있음" : "없음"}`
|
||||
);
|
||||
// console.log(
|
||||
// `✅ 파싱 완료 확인 - attachmentResult: ${attachmentResult ? "있음" : "없음"}`
|
||||
// );
|
||||
clearInterval(checkComplete);
|
||||
imap.end();
|
||||
resolve(attachmentResult);
|
||||
@@ -775,9 +775,9 @@ export class MailReceiveBasicService {
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(checkComplete);
|
||||
console.log(
|
||||
`⚠️ 타임아웃 - attachmentResult: ${attachmentResult ? "있음" : "없음"}`
|
||||
);
|
||||
// console.log(
|
||||
// `⚠️ 타임아웃 - attachmentResult: ${attachmentResult ? "있음" : "없음"}`
|
||||
// );
|
||||
imap.end();
|
||||
resolve(attachmentResult);
|
||||
}, 5000);
|
||||
@@ -840,7 +840,7 @@ export class MailReceiveBasicService {
|
||||
|
||||
imap.once("ready", () => {
|
||||
clearTimeout(timeout);
|
||||
console.log(`🔗 IMAP 연결 성공 - 메일 삭제 시작 (seqno=${seqno})`);
|
||||
// console.log(`🔗 IMAP 연결 성공 - 메일 삭제 시작 (seqno=${seqno})`);
|
||||
|
||||
imap.openBox("INBOX", false, (err: any) => {
|
||||
if (err) {
|
||||
@@ -857,7 +857,7 @@ export class MailReceiveBasicService {
|
||||
return reject(flagErr);
|
||||
}
|
||||
|
||||
console.log(`✓ 삭제 플래그 추가 완료 (seqno=${seqno})`);
|
||||
// console.log(`✓ 삭제 플래그 추가 완료 (seqno=${seqno})`);
|
||||
|
||||
// 삭제 플래그가 표시된 메일을 영구 삭제 (실제로는 휴지통으로 이동)
|
||||
imap.expunge((expungeErr: any) => {
|
||||
@@ -868,7 +868,7 @@ export class MailReceiveBasicService {
|
||||
return reject(expungeErr);
|
||||
}
|
||||
|
||||
console.log(`🗑️ 메일 삭제 완료: seqno=${seqno}`);
|
||||
// console.log(`🗑️ 메일 삭제 완료: seqno=${seqno}`);
|
||||
resolve({
|
||||
success: true,
|
||||
message: "메일이 삭제되었습니다.",
|
||||
@@ -888,8 +888,10 @@ export class MailReceiveBasicService {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
|
||||
console.log(`🔌 IMAP 연결 시도 중... (host=${config.host}, port=${config.port})`);
|
||||
// console.log(`🔌 IMAP 연결 시도 중... (host=${config.host}, port=${config.port})`);
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const mailReceiveBasicService = new MailReceiveBasicService();
|
||||
|
||||
@@ -36,11 +36,12 @@ export interface SendMailResult {
|
||||
|
||||
export interface BulkSendRequest {
|
||||
accountId: string;
|
||||
templateId: string;
|
||||
templateId?: string; // 템플릿 ID (선택)
|
||||
subject: string;
|
||||
customHtml?: string; // 직접 작성한 HTML (선택)
|
||||
recipients: Array<{
|
||||
email: string;
|
||||
variables: Record<string, string>;
|
||||
variables?: Record<string, string>; // 템플릿 사용 시에만 필요
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ class MailSendSimpleService {
|
||||
|
||||
// 🎯 수정된 컴포넌트가 있으면 덮어쓰기
|
||||
if (request.modifiedTemplateComponents && request.modifiedTemplateComponents.length > 0) {
|
||||
console.log('✏️ 수정된 템플릿 컴포넌트 사용:', request.modifiedTemplateComponents.length);
|
||||
// console.log('✏️ 수정된 템플릿 컴포넌트 사용:', request.modifiedTemplateComponents.length);
|
||||
template.components = request.modifiedTemplateComponents;
|
||||
}
|
||||
|
||||
@@ -106,15 +107,15 @@ class MailSendSimpleService {
|
||||
|
||||
// 4. 비밀번호 복호화
|
||||
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
||||
// console.log('🔐 비밀번호 복호화 완료');
|
||||
// console.log('🔐 암호화된 비밀번호 (일부):', account.smtpPassword.substring(0, 30) + '...');
|
||||
// console.log('🔐 복호화된 비밀번호 길이:', decryptedPassword.length);
|
||||
// // 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 연결 설정:', {
|
||||
// // console.log('📧 SMTP 연결 설정:', {
|
||||
// host: account.smtpHost,
|
||||
// port: account.smtpPort,
|
||||
// secure: isSecure,
|
||||
@@ -134,7 +135,7 @@ class MailSendSimpleService {
|
||||
greetingTimeout: 30000,
|
||||
});
|
||||
|
||||
console.log('📧 메일 발송 시도 중...');
|
||||
// console.log('📧 메일 발송 시도 중...');
|
||||
|
||||
// 6. 메일 발송 (CC, BCC, 첨부파일 지원)
|
||||
const mailOptions: any = {
|
||||
@@ -147,13 +148,13 @@ class MailSendSimpleService {
|
||||
// 참조(CC) 추가
|
||||
if (request.cc && request.cc.length > 0) {
|
||||
mailOptions.cc = request.cc.join(', ');
|
||||
// console.log('📧 참조(CC):', request.cc);
|
||||
// // console.log('📧 참조(CC):', request.cc);
|
||||
}
|
||||
|
||||
// 숨은참조(BCC) 추가
|
||||
if (request.bcc && request.bcc.length > 0) {
|
||||
mailOptions.bcc = request.bcc.join(', ');
|
||||
// console.log('🔒 숨은참조(BCC):', request.bcc);
|
||||
// // console.log('🔒 숨은참조(BCC):', request.bcc);
|
||||
}
|
||||
|
||||
// 첨부파일 추가 (한글 파일명 인코딩 처리)
|
||||
@@ -185,17 +186,17 @@ class MailSendSimpleService {
|
||||
}
|
||||
};
|
||||
});
|
||||
console.log('📎 첨부파일 (원본):', request.attachments.map((a: any) => a.filename.replace(/^\d+-\d+_/, '')));
|
||||
console.log('📎 첨부파일 (인코딩):', mailOptions.attachments.map((a: any) => a.filename));
|
||||
// console.log('📎 첨부파일 (원본):', request.attachments.map((a: any) => a.filename.replace(/^\d+-\d+_/, '')));
|
||||
// console.log('📎 첨부파일 (인코딩):', mailOptions.attachments.map((a: any) => a.filename));
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
|
||||
console.log('✅ 메일 발송 성공:', {
|
||||
messageId: info.messageId,
|
||||
accepted: info.accepted,
|
||||
rejected: info.rejected,
|
||||
});
|
||||
// console.log('✅ 메일 발송 성공:', {
|
||||
// messageId: info.messageId,
|
||||
// accepted: info.accepted,
|
||||
// rejected: info.rejected,
|
||||
// });
|
||||
|
||||
// 발송 이력 저장 (성공)
|
||||
try {
|
||||
@@ -438,17 +439,18 @@ class MailSendSimpleService {
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
console.log(`📧 대량 발송 시작: ${request.recipients.length}명`);
|
||||
// console.log(`📧 대량 발송 시작: ${request.recipients.length}명`);
|
||||
|
||||
// 순차 발송 (너무 빠르면 스팸으로 분류될 수 있음)
|
||||
for (const recipient of request.recipients) {
|
||||
try {
|
||||
const result = await this.sendMail({
|
||||
accountId: request.accountId,
|
||||
templateId: request.templateId,
|
||||
templateId: request.templateId, // 템플릿이 있으면 사용
|
||||
customHtml: request.customHtml, // 직접 작성한 HTML이 있으면 사용
|
||||
to: [recipient.email],
|
||||
subject: request.subject,
|
||||
variables: recipient.variables,
|
||||
variables: recipient.variables || {}, // 템플릿 사용 시에만 필요
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
@@ -480,7 +482,7 @@ class MailSendSimpleService {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log(`✅ 대량 발송 완료: 성공 ${successCount}, 실패 ${failedCount}`);
|
||||
// console.log(`✅ 대량 발송 완료: 성공 ${successCount}, 실패 ${failedCount}`);
|
||||
|
||||
return {
|
||||
total: request.recipients.length,
|
||||
@@ -502,13 +504,13 @@ class MailSendSimpleService {
|
||||
|
||||
// 비밀번호 복호화
|
||||
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
||||
// console.log('🔐 테스트용 비밀번호 복호화 완료');
|
||||
// console.log('🔐 복호화된 비밀번호 길이:', decryptedPassword.length);
|
||||
// // console.log('🔐 테스트용 비밀번호 복호화 완료');
|
||||
// // console.log('🔐 복호화된 비밀번호 길이:', decryptedPassword.length);
|
||||
|
||||
// 포트 465는 SSL/TLS를 사용해야 함
|
||||
const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
|
||||
|
||||
// console.log('🧪 SMTP 연결 테스트 시작:', {
|
||||
// // console.log('🧪 SMTP 연결 테스트 시작:', {
|
||||
// host: account.smtpHost,
|
||||
// port: account.smtpPort,
|
||||
// secure: isSecure,
|
||||
@@ -531,7 +533,7 @@ class MailSendSimpleService {
|
||||
// 연결 테스트
|
||||
await transporter.verify();
|
||||
|
||||
console.log('✅ SMTP 연결 테스트 성공');
|
||||
// console.log('✅ SMTP 연결 테스트 성공');
|
||||
return { success: true, message: 'SMTP 연결이 성공했습니다.' };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
@@ -53,7 +53,7 @@ class MailSentHistoryService {
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("발송 이력 저장:", history.id);
|
||||
// console.log("발송 이력 저장:", history.id);
|
||||
} catch (error) {
|
||||
console.error("발송 이력 저장 실패:", error);
|
||||
// 파일 저장 실패해도 history 객체는 반환 (메일 발송은 성공했으므로)
|
||||
@@ -86,7 +86,7 @@ class MailSentHistoryService {
|
||||
try {
|
||||
// 디렉토리가 없으면 빈 배열 반환
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
console.warn("메일 발송 이력 디렉토리가 없습니다:", SENT_MAIL_DIR);
|
||||
// console.warn("메일 발송 이력 디렉토리가 없습니다:", SENT_MAIL_DIR);
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -221,7 +221,7 @@ class MailSentHistoryService {
|
||||
async saveDraft(
|
||||
data: Partial<SentMailHistory> & { accountId: string }
|
||||
): Promise<SentMailHistory> {
|
||||
console.log("📥 백엔드에서 받은 임시 저장 데이터:", data);
|
||||
// console.log("📥 백엔드에서 받은 임시 저장 데이터:", data);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const draft: SentMailHistory = {
|
||||
@@ -243,7 +243,7 @@ class MailSentHistoryService {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
console.log("💾 저장할 draft 객체:", draft);
|
||||
// console.log("💾 저장할 draft 객체:", draft);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
@@ -256,7 +256,7 @@ class MailSentHistoryService {
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("💾 임시 저장:", draft.id);
|
||||
// console.log("💾 임시 저장:", draft.id);
|
||||
} catch (error) {
|
||||
console.error("임시 저장 실패:", error);
|
||||
throw error;
|
||||
@@ -291,7 +291,7 @@ class MailSentHistoryService {
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("✏️ 임시 저장 업데이트:", id);
|
||||
// console.log("✏️ 임시 저장 업데이트:", id);
|
||||
return updated;
|
||||
} catch (error) {
|
||||
console.error("임시 저장 업데이트 실패:", error);
|
||||
@@ -320,7 +320,7 @@ class MailSentHistoryService {
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("🗑️ 메일 삭제 (Soft Delete):", id);
|
||||
// console.log("🗑️ 메일 삭제 (Soft Delete):", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("메일 삭제 실패:", error);
|
||||
@@ -349,7 +349,7 @@ class MailSentHistoryService {
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("♻️ 메일 복구:", id);
|
||||
// console.log("♻️ 메일 복구:", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("메일 복구 실패:", error);
|
||||
@@ -369,7 +369,7 @@ class MailSentHistoryService {
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
console.log("🗑️ 메일 영구 삭제:", id);
|
||||
// console.log("🗑️ 메일 영구 삭제:", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("메일 영구 삭제 실패:", error);
|
||||
@@ -406,7 +406,7 @@ class MailSentHistoryService {
|
||||
if (deletedDate < thirtyDaysAgo) {
|
||||
fs.unlinkSync(filePath);
|
||||
deletedCount++;
|
||||
console.log("🗑️ 30일 지난 메일 자동 삭제:", mail.id);
|
||||
// console.log("🗑️ 30일 지난 메일 자동 삭제:", mail.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -173,7 +173,7 @@ class MailTemplateFileService {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// console.log(`📝 템플릿 저장 시도: ${id}, 크기: ${JSON.stringify(updated).length} bytes`);
|
||||
// // console.log(`📝 템플릿 저장 시도: ${id}, 크기: ${JSON.stringify(updated).length} bytes`);
|
||||
|
||||
await fs.writeFile(
|
||||
this.getTemplatePath(id),
|
||||
@@ -181,7 +181,7 @@ class MailTemplateFileService {
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
// console.log(`✅ 템플릿 저장 성공: ${id}`);
|
||||
// // console.log(`✅ 템플릿 저장 성공: ${id}`);
|
||||
return updated;
|
||||
} catch (error) {
|
||||
// console.error(`❌ 템플릿 저장 실패: ${id}`, error);
|
||||
|
||||
@@ -38,11 +38,11 @@ export default function MailAccountsPage() {
|
||||
if (Array.isArray(data)) {
|
||||
setAccounts(data);
|
||||
} else {
|
||||
console.error('API 응답이 배열이 아닙니다:', data);
|
||||
// console.error('API 응답이 배열이 아닙니다:', data);
|
||||
setAccounts([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('계정 로드 실패:', error);
|
||||
// console.error('계정 로드 실패:', error);
|
||||
setAccounts([]); // 에러 시 빈 배열로 설정
|
||||
// alert('계정 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -93,7 +93,7 @@ export default function MailAccountsPage() {
|
||||
await loadAccounts();
|
||||
alert('계정이 삭제되었습니다.');
|
||||
} catch (error) {
|
||||
console.error('계정 삭제 실패:', error);
|
||||
// console.error('계정 삭제 실패:', error);
|
||||
alert('계정 삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
@@ -104,7 +104,7 @@ export default function MailAccountsPage() {
|
||||
await updateMailAccount(account.id, { status: newStatus });
|
||||
await loadAccounts();
|
||||
} catch (error) {
|
||||
console.error('상태 변경 실패:', error);
|
||||
// console.error('상태 변경 실패:', error);
|
||||
alert('상태 변경에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
@@ -120,7 +120,7 @@ export default function MailAccountsPage() {
|
||||
alert(`❌ SMTP 연결 실패\n\n${result.message || '연결에 실패했습니다.'}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('연결 테스트 실패:', error);
|
||||
// console.error('연결 테스트 실패:', error);
|
||||
alert(`❌ SMTP 연결 테스트 실패\n\n${error.message || '알 수 없는 오류가 발생했습니다.'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -48,6 +48,8 @@ export default function BulkSendPage() {
|
||||
const [templates, setTemplates] = useState<MailTemplate[]>([]);
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<string>("");
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||
const [useTemplate, setUseTemplate] = useState<boolean>(true); // 템플릿 사용 여부
|
||||
const [customHtml, setCustomHtml] = useState<string>(""); // 직접 작성한 HTML
|
||||
const [subject, setSubject] = useState<string>("");
|
||||
const [recipients, setRecipients] = useState<RecipientData[]>([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
@@ -63,7 +65,7 @@ export default function BulkSendPage() {
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const data = await getMailAccounts();
|
||||
setAccounts(data.filter((acc) => acc.isActive));
|
||||
setAccounts(data.filter((acc) => acc.status === 'active'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
toast({
|
||||
@@ -164,7 +166,8 @@ export default function BulkSendPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedTemplateId) {
|
||||
// 템플릿 또는 직접 작성 중 하나는 있어야 함
|
||||
if (useTemplate && !selectedTemplateId) {
|
||||
toast({
|
||||
title: "템플릿 선택 필요",
|
||||
description: "사용할 템플릿을 선택해주세요.",
|
||||
@@ -173,6 +176,15 @@ export default function BulkSendPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!useTemplate && !customHtml.trim()) {
|
||||
toast({
|
||||
title: "내용 입력 필요",
|
||||
description: "메일 내용을 입력해주세요.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subject.trim()) {
|
||||
toast({
|
||||
title: "제목 입력 필요",
|
||||
@@ -197,7 +209,8 @@ export default function BulkSendPage() {
|
||||
try {
|
||||
await sendBulkMail({
|
||||
accountId: selectedAccountId,
|
||||
templateId: selectedTemplateId,
|
||||
templateId: useTemplate ? selectedTemplateId : undefined,
|
||||
customHtml: !useTemplate ? customHtml : undefined,
|
||||
subject,
|
||||
recipients,
|
||||
onProgress: (sent, total) => {
|
||||
@@ -287,21 +300,51 @@ example2@example.com,김철수,XYZ회사`;
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="template">템플릿</Label>
|
||||
<Select value={selectedTemplateId} onValueChange={setSelectedTemplateId}>
|
||||
<SelectTrigger id="template">
|
||||
<SelectValue placeholder="템플릿 선택" />
|
||||
<Label htmlFor="mode">발송 방식</Label>
|
||||
<Select value={useTemplate ? "template" : "custom"} onValueChange={(v) => setUseTemplate(v === "template")}>
|
||||
<SelectTrigger id="mode">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="template">템플릿 사용</SelectItem>
|
||||
<SelectItem value="custom">직접 작성</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{useTemplate ? (
|
||||
<div>
|
||||
<Label htmlFor="template">템플릿</Label>
|
||||
<Select value={selectedTemplateId} onValueChange={setSelectedTemplateId}>
|
||||
<SelectTrigger id="template">
|
||||
<SelectValue placeholder="템플릿 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Label htmlFor="customHtml">메일 내용</Label>
|
||||
<Textarea
|
||||
id="customHtml"
|
||||
value={customHtml}
|
||||
onChange={(e) => setCustomHtml(e.target.value)}
|
||||
placeholder="메일 내용을 작성하세요..."
|
||||
rows={10}
|
||||
className="text-sm"
|
||||
/>
|
||||
{/* <p className="mt-1 text-xs text-muted-foreground">
|
||||
HTML 태그를 사용할 수 있습니다
|
||||
</p> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="subject">제목</Label>
|
||||
<Input
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function MailDashboardPage() {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메일 통계 조회 실패:', error);
|
||||
// console.error('메일 통계 조회 실패:', error);
|
||||
// 기본값 사용
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function MailDashboardPage() {
|
||||
try {
|
||||
receivedTodayCount = await getTodayReceivedCount();
|
||||
} catch (error) {
|
||||
console.error('수신 메일 수 조회 실패:', error);
|
||||
// console.error('수신 메일 수 조회 실패:', error);
|
||||
// 실패 시 0으로 표시
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function MailDashboardPage() {
|
||||
successRate: mailStats.successRate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('통계 로드 실패:', error);
|
||||
// console.error('통계 로드 실패:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ export default function DraftsPage() {
|
||||
sortBy: "updatedAt",
|
||||
sortOrder: "desc",
|
||||
});
|
||||
console.log('📋 임시 저장 목록 조회:', response);
|
||||
console.log('📋 임시 저장 개수:', response.items.length);
|
||||
// console.log('📋 임시 저장 목록 조회:', response);
|
||||
// console.log('📋 임시 저장 개수:', response.items.length);
|
||||
setDrafts(response.items);
|
||||
} catch (error) {
|
||||
console.error("❌ 임시 저장 메일 로드 실패:", error);
|
||||
// console.error("❌ 임시 저장 메일 로드 실패:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export default function DraftsPage() {
|
||||
setDrafts(drafts.filter((d) => d.id !== id));
|
||||
setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id));
|
||||
} catch (error) {
|
||||
console.error("임시 저장 메일 삭제 실패:", error);
|
||||
// console.error("임시 저장 메일 삭제 실패:", error);
|
||||
alert("삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
@@ -85,7 +85,7 @@ export default function DraftsPage() {
|
||||
setSelectedIds([]);
|
||||
alert(result.message);
|
||||
} catch (error) {
|
||||
console.error("일괄 삭제 실패:", error);
|
||||
// console.error("일괄 삭제 실패:", error);
|
||||
alert("일괄 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setBulkDeleting(false);
|
||||
|
||||
@@ -61,6 +61,12 @@ export default function MailReceivePage() {
|
||||
const [filterStatus, setFilterStatus] = useState<string>("all"); // all, unread, read, attachment
|
||||
const [sortBy, setSortBy] = useState<string>("date-desc"); // date-desc, date-asc, from-asc, from-desc
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage] = useState(10);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [allMails, setAllMails] = useState<ReceivedMail[]>([]); // 전체 메일 저장
|
||||
|
||||
// 계정 목록 로드
|
||||
useEffect(() => {
|
||||
loadAccounts();
|
||||
@@ -69,17 +75,25 @@ export default function MailReceivePage() {
|
||||
// 계정 선택 시 메일 로드
|
||||
useEffect(() => {
|
||||
if (selectedAccountId) {
|
||||
setCurrentPage(1); // 계정 변경 시 첫 페이지로
|
||||
loadMails();
|
||||
}
|
||||
}, [selectedAccountId]);
|
||||
|
||||
// 페이지 변경 시 페이지네이션 재적용
|
||||
useEffect(() => {
|
||||
if (allMails.length > 0) {
|
||||
applyPagination(allMails);
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
// URL 파라미터에서 mailId 읽기 및 자동 선택
|
||||
useEffect(() => {
|
||||
const mailId = searchParams.get('mailId');
|
||||
const accountId = searchParams.get('accountId');
|
||||
|
||||
if (mailId && accountId) {
|
||||
console.log('📧 URL에서 메일 ID 감지:', mailId, accountId);
|
||||
// console.log('📧 URL에서 메일 ID 감지:', mailId, accountId);
|
||||
setSelectedAccountId(accountId);
|
||||
setSelectedMailId(mailId);
|
||||
// 메일 상세 로드는 handleMailClick에서 처리됨
|
||||
@@ -91,7 +105,7 @@ export default function MailReceivePage() {
|
||||
if (selectedMailId && mails.length > 0 && !selectedMailDetail) {
|
||||
const mail = mails.find(m => m.id === selectedMailId);
|
||||
if (mail) {
|
||||
console.log('🎯 URL에서 지정된 메일 자동 선택:', selectedMailId);
|
||||
// console.log('🎯 URL에서 지정된 메일 자동 선택:', selectedMailId);
|
||||
handleMailClick(mail);
|
||||
}
|
||||
}
|
||||
@@ -119,7 +133,7 @@ export default function MailReceivePage() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("계정 로드 실패:", error);
|
||||
// console.error("계정 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,36 +143,47 @@ export default function MailReceivePage() {
|
||||
setLoading(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const data = await getReceivedMails(selectedAccountId, 50);
|
||||
const data = await getReceivedMails(selectedAccountId, 200); // 더 많이 가져오기
|
||||
|
||||
// 현재 로컬에서 읽음 처리한 메일들의 상태를 유지
|
||||
setMails((prevMails) => {
|
||||
const localReadMailIds = new Set(
|
||||
prevMails.filter(m => m.isRead).map(m => m.id)
|
||||
);
|
||||
|
||||
return data.map(mail => ({
|
||||
...mail,
|
||||
// 로컬에서 읽음 처리했거나 서버에서 읽음 상태면 읽음으로 표시
|
||||
isRead: mail.isRead || localReadMailIds.has(mail.id)
|
||||
}));
|
||||
});
|
||||
const processedMails = data.map(mail => ({
|
||||
...mail,
|
||||
isRead: mail.isRead
|
||||
}));
|
||||
|
||||
setAllMails(processedMails); // 전체 메일 저장
|
||||
|
||||
// 페이지네이션 적용
|
||||
applyPagination(processedMails);
|
||||
|
||||
// 알림 갱신 이벤트 발생 (새 메일이 있을 수 있음)
|
||||
window.dispatchEvent(new CustomEvent('mail-received'));
|
||||
} catch (error) {
|
||||
console.error("메일 로드 실패:", error);
|
||||
// console.error("메일 로드 실패:", error);
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "메일을 불러오는데 실패했습니다."
|
||||
);
|
||||
setMails([]);
|
||||
setAllMails([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyPagination = (mailList: ReceivedMail[]) => {
|
||||
const totalItems = mailList.length;
|
||||
const totalPagesCalc = Math.ceil(totalItems / itemsPerPage);
|
||||
setTotalPages(totalPagesCalc);
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedMails = mailList.slice(startIndex, endIndex);
|
||||
|
||||
setMails(paginatedMails);
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!selectedAccountId) return;
|
||||
|
||||
@@ -208,7 +233,7 @@ export default function MailReceivePage() {
|
||||
setLoadingDetail(true);
|
||||
|
||||
// 즉시 로컬 상태 업데이트 (UI 반응성 향상)
|
||||
console.log('📧 메일 클릭:', mail.id, '현재 읽음 상태:', mail.isRead);
|
||||
// console.log('📧 메일 클릭:', mail.id, '현재 읽음 상태:', mail.isRead);
|
||||
setMails((prevMails) =>
|
||||
prevMails.map((m) =>
|
||||
m.id === mail.id ? { ...m, isRead: true } : m
|
||||
@@ -222,7 +247,7 @@ export default function MailReceivePage() {
|
||||
const accountId = `${mailIdParts[0]}-${mailIdParts[1]}`; // "account-1759310844272"
|
||||
const seqno = parseInt(mailIdParts[2], 10); // 13
|
||||
|
||||
console.log('🔍 추출된 accountId:', accountId, 'seqno:', seqno, '원본 mailId:', mail.id);
|
||||
// console.log('🔍 추출된 accountId:', accountId, 'seqno:', seqno, '원본 mailId:', mail.id);
|
||||
|
||||
const detail = await getMailDetail(accountId, seqno);
|
||||
setSelectedMailDetail(detail);
|
||||
@@ -230,18 +255,18 @@ export default function MailReceivePage() {
|
||||
// 읽음 처리
|
||||
if (!mail.isRead) {
|
||||
await markMailAsRead(accountId, seqno);
|
||||
console.log('✅ 읽음 처리 완료 - seqno:', seqno);
|
||||
// console.log('✅ 읽음 처리 완료 - seqno:', seqno);
|
||||
|
||||
// 서버 상태 동기화 (백그라운드) - IMAP 서버 반영 대기
|
||||
setTimeout(() => {
|
||||
if (selectedAccountId) {
|
||||
console.log('🔄 서버 상태 동기화 시작');
|
||||
// console.log('🔄 서버 상태 동기화 시작');
|
||||
loadMails();
|
||||
}
|
||||
}, 2000); // 2초로 증가
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메일 상세 로드 실패:', error);
|
||||
// console.error('메일 상세 로드 실패:', error);
|
||||
} finally {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
@@ -258,7 +283,7 @@ export default function MailReceivePage() {
|
||||
const accountId = `${mailIdParts[0]}-${mailIdParts[1]}`; // "account-1759310844272"
|
||||
const seqno = parseInt(mailIdParts[2], 10); // 10
|
||||
|
||||
console.log(`🗑️ 메일 삭제 시도: accountId=${accountId}, seqno=${seqno}`);
|
||||
// console.log(`🗑️ 메일 삭제 시도: accountId=${accountId}, seqno=${seqno}`);
|
||||
|
||||
// IMAP 서버에서 메일 삭제 (타임아웃 40초)
|
||||
const response = await apiClient.delete(`/mail/receive/${accountId}/${seqno}`, {
|
||||
@@ -274,10 +299,10 @@ export default function MailReceivePage() {
|
||||
setSelectedMailDetail(null);
|
||||
|
||||
alert("메일이 삭제되었습니다.");
|
||||
console.log("✅ 메일 삭제 완료");
|
||||
// console.log("✅ 메일 삭제 완료");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("메일 삭제 실패:", error);
|
||||
// console.error("메일 삭제 실패:", error);
|
||||
|
||||
let errorMessage = "메일 삭제에 실패했습니다.";
|
||||
|
||||
@@ -596,6 +621,72 @@ export default function MailReceivePage() {
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2 p-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
처음
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum;
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1;
|
||||
} else if (currentPage <= 3) {
|
||||
pageNum = i + 1;
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i;
|
||||
} else {
|
||||
pageNum = currentPage - 2 + i;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={pageNum}
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className="w-8 h-8 p-0"
|
||||
>
|
||||
{pageNum}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
마지막
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
@@ -663,16 +754,16 @@ export default function MailReceivePage() {
|
||||
return text;
|
||||
};
|
||||
|
||||
console.log('📧 답장 데이터:', {
|
||||
htmlBody: selectedMailDetail.htmlBody,
|
||||
textBody: selectedMailDetail.textBody,
|
||||
});
|
||||
// console.log('📧 답장 데이터:', {
|
||||
// htmlBody: selectedMailDetail.htmlBody,
|
||||
// textBody: selectedMailDetail.textBody,
|
||||
// });
|
||||
|
||||
// textBody 우선 사용 (순수 텍스트), 없으면 htmlBody에서 추출
|
||||
const bodyText = selectedMailDetail.textBody
|
||||
|| (selectedMailDetail.htmlBody ? stripHtml(selectedMailDetail.htmlBody) : "");
|
||||
|
||||
console.log('📧 변환된 본문:', bodyText);
|
||||
// console.log('📧 변환된 본문:', bodyText);
|
||||
|
||||
const replyData = {
|
||||
originalFrom: selectedMailDetail.from,
|
||||
@@ -716,16 +807,16 @@ export default function MailReceivePage() {
|
||||
return text;
|
||||
};
|
||||
|
||||
console.log('📧 전달 데이터:', {
|
||||
htmlBody: selectedMailDetail.htmlBody,
|
||||
textBody: selectedMailDetail.textBody,
|
||||
});
|
||||
// console.log('📧 전달 데이터:', {
|
||||
// htmlBody: selectedMailDetail.htmlBody,
|
||||
// textBody: selectedMailDetail.textBody,
|
||||
// });
|
||||
|
||||
// textBody 우선 사용 (순수 텍스트), 없으면 htmlBody에서 추출
|
||||
const bodyText = selectedMailDetail.textBody
|
||||
|| (selectedMailDetail.htmlBody ? stripHtml(selectedMailDetail.htmlBody) : "");
|
||||
|
||||
console.log('📧 변환된 본문:', bodyText);
|
||||
// console.log('📧 변환된 본문:', bodyText);
|
||||
|
||||
const forwardData = {
|
||||
originalFrom: selectedMailDetail.from,
|
||||
|
||||
@@ -155,7 +155,7 @@ ${data.originalBody}`;
|
||||
// URL에서 파라미터 제거 (깔끔하게)
|
||||
router.replace("/admin/mail/send");
|
||||
} catch (error) {
|
||||
console.error("답장/전달 데이터 파싱 실패:", error);
|
||||
// console.error("답장/전달 데이터 파싱 실패:", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -200,21 +200,21 @@ ${data.originalBody}`;
|
||||
// 계정이 선택되지 않았고, 활성 계정이 있으면 첫 번째 계정 자동 선택
|
||||
if (!selectedAccountId && activeAccounts.length > 0) {
|
||||
setSelectedAccountId(activeAccounts[0].id);
|
||||
console.log('🔧 첫 번째 계정 자동 선택:', activeAccounts[0].email);
|
||||
// console.log('🔧 첫 번째 계정 자동 선택:', activeAccounts[0].email);
|
||||
}
|
||||
|
||||
console.log('📦 데이터 로드 완료:', {
|
||||
accounts: accountsData.length,
|
||||
templates: templatesData.length,
|
||||
templatesDetail: templatesData.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
componentsCount: t.components?.length || 0
|
||||
}))
|
||||
});
|
||||
|
||||
// console.log('📦 데이터 로드 완료:', {
|
||||
// accounts: accountsData.length,
|
||||
// templates: templatesData.length,
|
||||
// templatesDetail: templatesData.map(t => ({
|
||||
// id: t.id,
|
||||
// name: t.name,
|
||||
// componentsCount: t.components?.length || 0
|
||||
// }))
|
||||
// });
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('❌ 데이터 로드 실패:', err);
|
||||
// console.error('❌ 데이터 로드 실패:', err);
|
||||
toast({
|
||||
title: "데이터 로드 실패",
|
||||
description: err.message,
|
||||
@@ -259,7 +259,7 @@ ${data.originalBody}`;
|
||||
|
||||
setLastSaved(new Date());
|
||||
} catch (error) {
|
||||
console.error('임시 저장 실패:', error);
|
||||
// console.error('임시 저장 실패:', error);
|
||||
} finally {
|
||||
setAutoSaving(false);
|
||||
}
|
||||
@@ -280,7 +280,7 @@ ${data.originalBody}`;
|
||||
|
||||
// "__custom__"는 직접 작성을 의미
|
||||
if (templateId === "__custom__") {
|
||||
console.log('✏️ 직접 작성 모드');
|
||||
// console.log('✏️ 직접 작성 모드');
|
||||
setSelectedTemplateId("");
|
||||
setTemplateVariables([]);
|
||||
setVariables({});
|
||||
@@ -289,20 +289,20 @@ ${data.originalBody}`;
|
||||
|
||||
try {
|
||||
// 🎯 원본 템플릿을 API에서 다시 로드 (수정사항 초기화)
|
||||
console.log('🔃 원본 템플릿 API에서 재로드 중...');
|
||||
// console.log('🔃 원본 템플릿 API에서 재로드 중...');
|
||||
const freshTemplates = await getMailTemplates();
|
||||
const template = freshTemplates.find((t) => t.id === templateId);
|
||||
|
||||
console.log('📋 찾은 템플릿:', {
|
||||
found: !!template,
|
||||
templateId,
|
||||
availableTemplates: freshTemplates.length,
|
||||
template: template ? {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
componentsCount: template.components?.length || 0
|
||||
} : null
|
||||
});
|
||||
// console.log('📋 찾은 템플릿:', {
|
||||
// found: !!template,
|
||||
// templateId,
|
||||
// availableTemplates: freshTemplates.length,
|
||||
// template: template ? {
|
||||
// id: template.id,
|
||||
// name: template.name,
|
||||
// componentsCount: template.components?.length || 0
|
||||
// } : null
|
||||
// });
|
||||
|
||||
if (template) {
|
||||
// 🎯 templates state도 원본으로 업데이트 (깨끗한 상태)
|
||||
@@ -318,18 +318,18 @@ ${data.originalBody}`;
|
||||
});
|
||||
setVariables(initialVars);
|
||||
|
||||
console.log('✅ 원본 템플릿 적용 완료 (깨끗한 상태):', {
|
||||
subject: template.subject,
|
||||
variables: vars
|
||||
});
|
||||
// console.log('✅ 원본 템플릿 적용 완료 (깨끗한 상태):', {
|
||||
// subject: template.subject,
|
||||
// variables: vars
|
||||
// });
|
||||
} else {
|
||||
setSelectedTemplateId("");
|
||||
setTemplateVariables([]);
|
||||
setVariables({});
|
||||
console.warn('⚠️ 템플릿을 찾을 수 없음');
|
||||
// console.warn('⚠️ 템플릿을 찾을 수 없음');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 템플릿 재로드 실패:', error);
|
||||
// console.error('❌ 템플릿 재로드 실패:', error);
|
||||
toast({
|
||||
title: "템플릿 로드 실패",
|
||||
description: "템플릿을 불러오는 중 오류가 발생했습니다.",
|
||||
@@ -457,7 +457,7 @@ ${data.originalBody}`;
|
||||
const currentTemplate = templates.find((t) => t.id === selectedTemplateId);
|
||||
if (currentTemplate) {
|
||||
formData.append("modifiedTemplateComponents", JSON.stringify(currentTemplate.components));
|
||||
console.log('📤 수정된 템플릿 컴포넌트 전송:', currentTemplate.components.length);
|
||||
// console.log('📤 수정된 템플릿 컴포넌트 전송:', currentTemplate.components.length);
|
||||
}
|
||||
}
|
||||
formData.append("to", JSON.stringify(to));
|
||||
@@ -485,11 +485,11 @@ ${data.originalBody}`;
|
||||
const originalFileNames = attachments.map(file => {
|
||||
// 파일명 정규화 (NFD → NFC)
|
||||
const normalizedName = file.name.normalize('NFC');
|
||||
console.log('📎 파일명 정규화:', file.name, '->', normalizedName);
|
||||
// console.log('📎 파일명 정규화:', file.name, '->', normalizedName);
|
||||
return normalizedName;
|
||||
});
|
||||
formData.append("fileNames", JSON.stringify(originalFileNames));
|
||||
console.log('📎 전송할 정규화된 파일명들:', originalFileNames);
|
||||
// console.log('📎 전송할 정규화된 파일명들:', originalFileNames);
|
||||
}
|
||||
|
||||
// API 호출 (FormData 전송)
|
||||
@@ -573,16 +573,16 @@ ${data.originalBody}`;
|
||||
templateId: selectedTemplateId || undefined,
|
||||
};
|
||||
|
||||
console.log('💾 임시 저장 데이터:', draftData);
|
||||
// console.log('💾 임시 저장 데이터:', draftData);
|
||||
|
||||
if (draftId) {
|
||||
// 기존 임시 저장 업데이트
|
||||
await updateDraft(draftId, draftData);
|
||||
console.log('✏️ 임시 저장 업데이트 완료:', draftId);
|
||||
// console.log('✏️ 임시 저장 업데이트 완료:', draftId);
|
||||
} else {
|
||||
// 새로운 임시 저장
|
||||
const savedDraft = await saveDraft(draftData);
|
||||
console.log('💾 임시 저장 완료:', savedDraft);
|
||||
// console.log('💾 임시 저장 완료:', savedDraft);
|
||||
if (savedDraft && savedDraft.id) {
|
||||
setDraftId(savedDraft.id);
|
||||
}
|
||||
@@ -596,7 +596,7 @@ ${data.originalBody}`;
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('❌ 임시 저장 실패:', err);
|
||||
// console.error('❌ 임시 저장 실패:', err);
|
||||
toast({
|
||||
title: "임시 저장 실패",
|
||||
description: err.message || "임시 저장 중 오류가 발생했습니다.",
|
||||
@@ -701,15 +701,15 @@ ${data.originalBody}`;
|
||||
if (selectedTemplateId) {
|
||||
const template = templates.find((t) => t.id === selectedTemplateId);
|
||||
if (template) {
|
||||
console.log('🎨 템플릿 미리보기:', {
|
||||
templateId: selectedTemplateId,
|
||||
templateName: template.name,
|
||||
componentsCount: template.components?.length || 0,
|
||||
components: template.components,
|
||||
variables
|
||||
});
|
||||
// console.log('🎨 템플릿 미리보기:', {
|
||||
// templateId: selectedTemplateId,
|
||||
// templateName: template.name,
|
||||
// componentsCount: template.components?.length || 0,
|
||||
// components: template.components,
|
||||
// variables
|
||||
// });
|
||||
const html = renderTemplateToHtml(template, variables);
|
||||
console.log('📄 생성된 HTML:', html.substring(0, 200) + '...');
|
||||
// console.log('📄 생성된 HTML:', html.substring(0, 200) + '...');
|
||||
|
||||
// 추가 메시지가 있으면 병합
|
||||
if (customHtml && customHtml.trim()) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ export default function MailTemplatesPage() {
|
||||
const data = await getMailTemplates();
|
||||
setTemplates(data);
|
||||
} catch (error) {
|
||||
console.error('템플릿 로드 실패:', error);
|
||||
// console.error('템플릿 로드 실패:', error);
|
||||
alert('템플릿 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -110,7 +110,7 @@ export default function MailTemplatesPage() {
|
||||
await loadTemplates();
|
||||
alert('템플릿이 삭제되었습니다.');
|
||||
} catch (error) {
|
||||
console.error('템플릿 삭제 실패:', error);
|
||||
// console.error('템플릿 삭제 실패:', error);
|
||||
alert('템플릿 삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
@@ -126,7 +126,7 @@ export default function MailTemplatesPage() {
|
||||
await loadTemplates();
|
||||
alert('템플릿이 복사되었습니다.');
|
||||
} catch (error) {
|
||||
console.error('템플릿 복사 실패:', error);
|
||||
// console.error('템플릿 복사 실패:', error);
|
||||
alert('템플릿 복사에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function TrashPage() {
|
||||
});
|
||||
setTrashedMails(response.items);
|
||||
} catch (error) {
|
||||
console.error("휴지통 메일 로드 실패:", error);
|
||||
// console.error("휴지통 메일 로드 실패:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default function TrashPage() {
|
||||
await restoreMail(id);
|
||||
setTrashedMails(trashedMails.filter((m) => m.id !== id));
|
||||
} catch (error) {
|
||||
console.error("메일 복구 실패:", error);
|
||||
// console.error("메일 복구 실패:", error);
|
||||
alert("복구에 실패했습니다.");
|
||||
} finally {
|
||||
setRestoring(null);
|
||||
@@ -55,7 +55,7 @@ export default function TrashPage() {
|
||||
await permanentlyDeleteMail(id);
|
||||
setTrashedMails(trashedMails.filter((m) => m.id !== id));
|
||||
} catch (error) {
|
||||
console.error("메일 영구 삭제 실패:", error);
|
||||
// console.error("메일 영구 삭제 실패:", error);
|
||||
alert("삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
@@ -71,7 +71,7 @@ export default function TrashPage() {
|
||||
setTrashedMails([]);
|
||||
alert("휴지통을 비웠습니다.");
|
||||
} catch (error) {
|
||||
console.error("휴지통 비우기 실패:", error);
|
||||
// console.error("휴지통 비우기 실패:", error);
|
||||
alert("일부 메일 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -348,11 +348,12 @@ export async function sendMail(data: SendMailDto): Promise<MailSendResult> {
|
||||
*/
|
||||
export interface BulkSendRequest {
|
||||
accountId: string;
|
||||
templateId: string;
|
||||
templateId?: string; // 템플릿 ID (선택)
|
||||
customHtml?: string; // 직접 작성한 HTML (선택)
|
||||
subject: string;
|
||||
recipients: Array<{
|
||||
email: string;
|
||||
variables: Record<string, string>;
|
||||
variables?: Record<string, string>; // 템플릿 사용 시에만 필요
|
||||
}>;
|
||||
onProgress?: (sent: number, total: number) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user