ui 고치기 전 세이브
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "12b583c9-a6b2-4c7f-8340-fd0e700aa32e",
|
||||
"sentAt": "2025-10-22T05:17:38.303Z",
|
||||
"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;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹㅇ리'ㅐㅔ'ㅑ678463ㅎㄱ휼췇흍츄</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border: 1px solid #ccc; padding: 15px; margin: 10px 0; background-color: #f9f9f9;\">\r\n <p><strong>---------- 전달된 메시지 ----------</strong></p>\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\r\n <p><strong>제목:</strong> ㅏㅣ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<74dbd467-6185-024d-dd60-bf4459ff9ea4@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": [],
|
||||
"deletedAt": "2025-10-22T06:36:10.876Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "375f2326-ca86-468a-bfc3-2d4c3825577b",
|
||||
"sentAt": "2025-10-22T04:57:39.706Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"\"이희진\" <zian9227@naver.com>"
|
||||
],
|
||||
"subject": "Re: ㅏㅣ",
|
||||
"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 <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\r\n <p><strong>제목:</strong> ㅏㅣ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<f085efa6-2668-0293-57de-88b1e7009dd1@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "386e334a-df76-440c-ae8a-9bf06982fdc8",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Fwd: ㄴ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n</p>\n </div>\n ",
|
||||
"sentAt": "2025-10-22T07:04:27.192Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:04:57.280Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "3d411dc4-69a6-4236-b878-9693dff881be",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Re: ㄴ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">undefined</p>\n </div>\n ",
|
||||
"sentAt": "2025-10-22T06:56:51.060Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:56:51.060Z"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "3e30a264-8431-44c7-96ef-eed551e66a11",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Fwd: ㄴ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\"></p>\n </div>\n ",
|
||||
"sentAt": "2025-10-22T06:57:53.335Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:00:23.394Z"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "5bfb2acd-023a-4865-a738-2900179db5fb",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Fwd: ㄴ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n</p>\n </div>\n ",
|
||||
"sentAt": "2025-10-22T07:03:09.080Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T07:03:39.150Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "683c1323-1895-403a-bb9a-4e111a8909f6",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Re: ㄴ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">undefined</p>\n </div>\n ",
|
||||
"sentAt": "2025-10-22T06:54:55.097Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:54:55.097Z"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "7bed27d5-dae4-4ba8-85d0-c474c4fb907a",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Fwd: ㅏㅣ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\n <p><strong>제목:</strong> ㅏㅣ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n undefined\n </div>\n ",
|
||||
"sentAt": "2025-10-22T06:41:52.984Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:46:23.051Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "84ee9619-49ff-4f61-a7fa-0bb0b0b7199a",
|
||||
"sentAt": "2025-10-22T04:27:51.044Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"\"이희진\" <zian9227@naver.com>"
|
||||
],
|
||||
"subject": "Re: ㅅㄷㄴㅅ",
|
||||
"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 <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:03:03</p>\r\n <p><strong>제목:</strong> ㅅㄷㄴㅅ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<5fa451ff-7d29-7da4-ce56-ca7391c147af@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "8990ea86-3112-4e7c-b3e0-8b494181c4e0",
|
||||
"accountName": "",
|
||||
"accountEmail": "",
|
||||
"to": [],
|
||||
"subject": "",
|
||||
"htmlContent": "",
|
||||
"sentAt": "2025-10-22T06:17:31.379Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:17:31.379Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "89a32ace-f39b-44fa-b614-c65d96548f92",
|
||||
"sentAt": "2025-10-22T03:49:48.461Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"subject": "Fwd: 기상청 API허브 회원가입 인증번호",
|
||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\"><br> <br/><br/><br> <div style=\"border: 1px solid #ccc; padding: 15px; margin: 10px 0; background-color: #f9f9f9;\"><br> <p><strong>---------- 전달된 메시지 ----------</strong></p><br> <p><strong>보낸 사람:</strong> \"기상청 API허브\" <noreply@apihube.kma.go.kr></p><br> <p><strong>날짜:</strong> 2025. 10. 13. 오후 4:26:45</p><br> <p><strong>제목:</strong> 기상청 API허브 회원가입 인증번호</p><br> <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" /><br> undefined<br> </div><br> </p>\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<9b36ce56-4ef1-cf0c-1f39-2c73bcb521da@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "99703f2c-740c-492e-a866-a04289a9b699",
|
||||
"accountName": "",
|
||||
"accountEmail": "",
|
||||
"to": [],
|
||||
"subject": "",
|
||||
"htmlContent": "",
|
||||
"sentAt": "2025-10-22T06:20:08.450Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:20:08.450Z",
|
||||
"deletedAt": "2025-10-22T06:36:07.797Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e",
|
||||
"sentAt": "2025-10-22T04:31:17.175Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"\"이희진\" <zian9227@naver.com>"
|
||||
],
|
||||
"subject": "Re: ㅅㄷㄴㅅ",
|
||||
"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 <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:03:03</p>\r\n <p><strong>제목:</strong> ㅅㄷㄴㅅ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<0f215ba8-a1e4-8c5a-f43f-962f0717c161@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "9d0b9fcf-cabf-4053-b6b6-6e110add22de",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Re: ㅏㅣ",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\n <p><strong>제목:</strong> ㅏㅣ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">undefined</p>\n </div>\n ",
|
||||
"sentAt": "2025-10-22T06:50:04.224Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:50:04.224Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "b293e530-2b2d-4b8a-8081-d103fab5a13f",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Re: 수신메일확인용",
|
||||
"htmlContent": "\n <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 13. 오전 10:40:30</p>\n <p><strong>제목:</strong> 수신메일확인용</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n undefined\n </div>\n ",
|
||||
"sentAt": "2025-10-22T06:47:53.815Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:48:53.876Z"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "e3501abc-cd31-4b20-bb02-3c7ddbe54eb8",
|
||||
"accountName": "",
|
||||
"accountEmail": "",
|
||||
"to": [],
|
||||
"subject": "",
|
||||
"htmlContent": "",
|
||||
"sentAt": "2025-10-22T06:15:02.128Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:15:02.128Z"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "e93848a8-6901-44c4-b4db-27c8d2aeb8dd",
|
||||
"sentAt": "2025-10-22T04:28:42.686Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"\"권은아\" <chna8137s@gmail.com>"
|
||||
],
|
||||
"subject": "Re: 매우 졸린 오후예요",
|
||||
"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;\">호홋 답장 기능을 구현했다죵<br>얼른 퇴근하고 싪네여</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"권은아\" <chna8137s@gmail.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:10:37</p>\r\n <p><strong>제목:</strong> 매우 졸린 오후예요</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"attachments": [
|
||||
{
|
||||
"filename": "test용 이미지2.png",
|
||||
"originalName": "test용 이미지2.png",
|
||||
"size": 0,
|
||||
"path": "/app/uploads/mail-attachments/1761107318152-717716316.png",
|
||||
"mimetype": "image/png"
|
||||
}
|
||||
],
|
||||
"status": "success",
|
||||
"messageId": "<19981423-259b-0a50-e76d-23c860692c16@wace.me>",
|
||||
"accepted": [
|
||||
"chna8137s@gmail.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "eb92ed00-cc4f-4cc8-94c9-9bef312d16db",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "메일 임시저장 테스트 4",
|
||||
"htmlContent": "asd",
|
||||
"sentAt": "2025-10-22T06:21:40.019Z",
|
||||
"status": "draft",
|
||||
"isDraft": true,
|
||||
"updatedAt": "2025-10-22T06:21:40.019Z",
|
||||
"deletedAt": "2025-10-22T06:36:05.306Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "fcea6149-a098-4212-aa00-baef0cc083d6",
|
||||
"sentAt": "2025-10-22T04:24:54.126Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"\"DHS\" <ddhhss0603@gmail.com>"
|
||||
],
|
||||
"subject": "Re: 안녕하세여",
|
||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">어떻게 가는지 궁금한데 이따가 화면 보여주세영</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"DHS\" <ddhhss0603@gmail.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:09:49</p>\r\n <p><strong>제목:</strong> 안녕하세여</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"status": "success",
|
||||
"messageId": "<c24b04f0-b958-5e0b-4cc7-2bff30f23c2c@wace.me>",
|
||||
"accepted": [
|
||||
"ddhhss0603@gmail.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082",
|
||||
"sentAt": "2025-10-22T04:29:14.738Z",
|
||||
"accountId": "account-1759310844272",
|
||||
"accountName": "이희진",
|
||||
"accountEmail": "hjlee@wace.me",
|
||||
"to": [
|
||||
"\"이희진\" <zian9227@naver.com>"
|
||||
],
|
||||
"subject": "Re: ㅅㄷㄴㅅ",
|
||||
"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 <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:03:03</p>\r\n <p><strong>제목:</strong> ㅅㄷㄴㅅ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
||||
"attachments": [
|
||||
{
|
||||
"filename": "test용 이미지2.png",
|
||||
"originalName": "test용 이미지2.png",
|
||||
"size": 0,
|
||||
"path": "/app/uploads/mail-attachments/1761107350246-298369766.png",
|
||||
"mimetype": "image/png"
|
||||
}
|
||||
],
|
||||
"status": "success",
|
||||
"messageId": "<e68a0501-f79a-8713-a625-e882f711b30d@wace.me>",
|
||||
"accepted": [
|
||||
"zian9227@naver.com"
|
||||
],
|
||||
"rejected": []
|
||||
}
|
||||
385
backend-node/package-lock.json
generated
385
backend-node/package-lock.json
generated
@@ -31,6 +31,8 @@
|
||||
"nodemailer": "^6.10.1",
|
||||
"oracledb": "^6.9.0",
|
||||
"pg": "^8.16.3",
|
||||
"quill": "^2.0.3",
|
||||
"react-quill": "^2.0.0",
|
||||
"redis": "^4.6.10",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.11.0"
|
||||
@@ -3433,6 +3435,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/quill": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
|
||||
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parchment": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/quill/node_modules/parchment": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
@@ -4437,6 +4454,24 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-define-property": "^1.0.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -4610,6 +4645,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
@@ -4944,6 +4988,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
|
||||
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arguments": "^1.1.1",
|
||||
"is-date-object": "^1.0.5",
|
||||
"is-regex": "^1.1.4",
|
||||
"object-is": "^1.1.5",
|
||||
"object-keys": "^1.1.1",
|
||||
"regexp.prototype.flags": "^1.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -4988,6 +5052,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
@@ -5000,6 +5081,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -5554,6 +5652,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -5689,6 +5793,12 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -5696,6 +5806,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -5997,6 +6113,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
@@ -6249,6 +6374,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -6563,6 +6700,22 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
@@ -6599,6 +6752,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-date-object": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
|
||||
"integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
@@ -6701,6 +6870,24 @@
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"gopd": "^1.2.0",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -7658,6 +7845,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -7670,6 +7875,13 @@
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
@@ -8292,6 +8504,31 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-is": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -8436,6 +8673,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parchment": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
|
||||
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -8960,6 +9203,35 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quill": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
||||
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parchment": "^3.0.0",
|
||||
"quill-delta": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-delta": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
|
||||
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.3.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -9003,6 +9275,67 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-quill": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/quill": "^1.3.10",
|
||||
"lodash": "^4.17.4",
|
||||
"quill": "^1.3.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16 || ^17 || ^18",
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-quill/node_modules/eventemitter3": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-quill/node_modules/fast-diff": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/react-quill/node_modules/parchment": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/react-quill/node_modules/quill": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
|
||||
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"clone": "^2.1.1",
|
||||
"deep-equal": "^1.0.1",
|
||||
"eventemitter3": "^2.0.3",
|
||||
"extend": "^3.0.2",
|
||||
"parchment": "^1.1.4",
|
||||
"quill-delta": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-quill/node_modules/quill-delta": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deep-equal": "^1.0.1",
|
||||
"extend": "^3.0.2",
|
||||
"fast-diff": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
@@ -9054,6 +9387,26 @@
|
||||
"@redis/time-series": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"define-properties": "^1.2.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"set-function-name": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -9325,6 +9678,38 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
|
||||
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"functions-have-names": "^1.2.3",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
"nodemailer": "^6.10.1",
|
||||
"oracledb": "^6.9.0",
|
||||
"pg": "^8.16.3",
|
||||
"quill": "^2.0.3",
|
||||
"react-quill": "^2.0.0",
|
||||
"redis": "^4.6.10",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.11.0"
|
||||
|
||||
@@ -272,6 +272,28 @@ app.listen(PORT, HOST, async () => {
|
||||
} catch (error) {
|
||||
logger.error(`❌ 리스크/알림 자동 갱신 시작 실패:`, error);
|
||||
}
|
||||
|
||||
// 메일 자동 삭제 (30일 지난 삭제된 메일) - 매일 새벽 2시 실행
|
||||
try {
|
||||
const cron = await import("node-cron");
|
||||
const { mailSentHistoryService } = await import(
|
||||
"./services/mailSentHistoryService"
|
||||
);
|
||||
|
||||
cron.schedule("0 2 * * *", async () => {
|
||||
try {
|
||||
logger.info("🗑️ 30일 지난 삭제된 메일 자동 삭제 시작...");
|
||||
const deletedCount = await mailSentHistoryService.cleanupOldDeletedMails();
|
||||
logger.info(`✅ 30일 지난 메일 ${deletedCount}개 자동 삭제 완료`);
|
||||
} catch (error) {
|
||||
logger.error("❌ 메일 자동 삭제 실패:", error);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`⏰ 메일 자동 삭제 스케줄러가 시작되었습니다. (매일 새벽 2시)`);
|
||||
} catch (error) {
|
||||
logger.error(`❌ 메일 자동 삭제 스케줄러 시작 실패:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -217,5 +217,33 @@ export class MailReceiveBasicController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/mail/receive/:accountId/:seqno
|
||||
* IMAP 서버에서 메일 삭제
|
||||
*/
|
||||
async deleteMail(req: Request, res: Response) {
|
||||
try {
|
||||
const { accountId, seqno } = req.params;
|
||||
const seqnoNumber = parseInt(seqno, 10);
|
||||
|
||||
if (isNaN(seqnoNumber)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 메일 번호입니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.mailReceiveService.deleteMail(accountId, seqnoNumber);
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (error: unknown) {
|
||||
console.error('메일 삭제 실패:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '메일 삭제 실패',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,54 @@ export class MailSendSimpleController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 대량 메일 발송
|
||||
*/
|
||||
async sendBulkMail(req: Request, res: Response) {
|
||||
try {
|
||||
const { accountId, templateId, subject, recipients } = req.body;
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if (!accountId || !templateId || !subject || !recipients || !Array.isArray(recipients)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 파라미터가 누락되었습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
if (recipients.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '수신자가 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📧 대량 발송 요청: ${recipients.length}명`);
|
||||
|
||||
// 대량 발송 실행
|
||||
const result = await mailSendSimpleService.sendBulkMail({
|
||||
accountId,
|
||||
templateId,
|
||||
subject,
|
||||
recipients,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: `${result.success}/${result.total} 건 발송 완료`,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('❌ 대량 발송 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '대량 발송 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 연결 테스트
|
||||
*/
|
||||
|
||||
@@ -11,12 +11,14 @@ export class MailSentHistoryController {
|
||||
page: req.query.page ? parseInt(req.query.page as string) : undefined,
|
||||
limit: req.query.limit ? parseInt(req.query.limit as string) : undefined,
|
||||
searchTerm: req.query.searchTerm as string | undefined,
|
||||
status: req.query.status as 'success' | 'failed' | 'all' | undefined,
|
||||
status: req.query.status as 'success' | 'failed' | 'draft' | 'all' | undefined,
|
||||
accountId: req.query.accountId as string | undefined,
|
||||
startDate: req.query.startDate as string | undefined,
|
||||
endDate: req.query.endDate as string | undefined,
|
||||
sortBy: req.query.sortBy as 'sentAt' | 'subject' | undefined,
|
||||
sortBy: req.query.sortBy as 'sentAt' | 'subject' | 'updatedAt' | undefined,
|
||||
sortOrder: req.query.sortOrder as 'asc' | 'desc' | undefined,
|
||||
includeDeleted: req.query.includeDeleted === 'true',
|
||||
onlyDeleted: req.query.onlyDeleted === 'true',
|
||||
};
|
||||
|
||||
const result = await mailSentHistoryService.getSentMailList(query);
|
||||
@@ -112,6 +114,144 @@ export class MailSentHistoryController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 임시 저장 (Draft)
|
||||
*/
|
||||
async saveDraft(req: Request, res: Response) {
|
||||
try {
|
||||
const draft = await mailSentHistoryService.saveDraft(req.body);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: draft,
|
||||
message: '임시 저장되었습니다.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('임시 저장 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '임시 저장 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 임시 저장 업데이트
|
||||
*/
|
||||
async updateDraft(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '임시 저장 ID가 필요합니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await mailSentHistoryService.updateDraft(id, req.body);
|
||||
|
||||
if (!updated) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '임시 저장을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: updated,
|
||||
message: '임시 저장이 업데이트되었습니다.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('임시 저장 업데이트 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '임시 저장 업데이트 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 복구
|
||||
*/
|
||||
async restoreMail(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '메일 ID가 필요합니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const success = await mailSentHistoryService.restoreMail(id);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '복구할 메일을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '메일이 복구되었습니다.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('메일 복구 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '메일 복구 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 영구 삭제
|
||||
*/
|
||||
async permanentlyDelete(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '메일 ID가 필요합니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const success = await mailSentHistoryService.permanentlyDeleteMail(id);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '삭제할 메일을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '메일이 영구 삭제되었습니다.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('메일 영구 삭제 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '메일 영구 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
@@ -134,6 +274,117 @@ export class MailSentHistoryController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 삭제
|
||||
*/
|
||||
async bulkDelete(req: Request, res: Response) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '삭제할 메일 ID 목록이 필요합니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
ids.map((id: string) => mailSentHistoryService.deleteSentMail(id))
|
||||
);
|
||||
|
||||
const successCount = results.filter((r) => r.status === 'fulfilled' && r.value).length;
|
||||
const failCount = results.length - successCount;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${successCount}개 메일 삭제 완료 (실패: ${failCount}개)`,
|
||||
data: { successCount, failCount },
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('일괄 삭제 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '일괄 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 영구 삭제
|
||||
*/
|
||||
async bulkPermanentDelete(req: Request, res: Response) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '영구 삭제할 메일 ID 목록이 필요합니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
ids.map((id: string) => mailSentHistoryService.permanentlyDeleteMail(id))
|
||||
);
|
||||
|
||||
const successCount = results.filter((r) => r.status === 'fulfilled' && r.value).length;
|
||||
const failCount = results.length - successCount;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${successCount}개 메일 영구 삭제 완료 (실패: ${failCount}개)`,
|
||||
data: { successCount, failCount },
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('일괄 영구 삭제 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '일괄 영구 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 복구
|
||||
*/
|
||||
async bulkRestore(req: Request, res: Response) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '복구할 메일 ID 목록이 필요합니다.',
|
||||
});
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
ids.map((id: string) => mailSentHistoryService.restoreMail(id))
|
||||
);
|
||||
|
||||
const successCount = results.filter((r) => r.status === 'fulfilled' && r.value).length;
|
||||
const failCount = results.length - successCount;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${successCount}개 메일 복구 완료 (실패: ${failCount}개)`,
|
||||
data: { successCount, failCount },
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error('일괄 복구 실패:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '일괄 복구 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mailSentHistoryController = new MailSentHistoryController();
|
||||
|
||||
@@ -27,6 +27,9 @@ router.get('/:accountId/:seqno/attachment/:index', (req, res) => {
|
||||
// 메일 읽음 표시 - 구체적인 경로
|
||||
router.post('/:accountId/:seqno/mark-read', (req, res) => controller.markAsRead(req, res));
|
||||
|
||||
// 메일 삭제 - 구체적인 경로
|
||||
router.delete('/:accountId/:seqno', (req, res) => controller.deleteMail(req, res));
|
||||
|
||||
// 메일 상세 조회 - /:accountId보다 먼저 정의해야 함
|
||||
router.get('/:accountId/:seqno', (req, res) => controller.getMailDetail(req, res));
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ router.post(
|
||||
(req, res) => mailSendSimpleController.sendMail(req, res)
|
||||
);
|
||||
|
||||
// POST /api/mail/send/bulk - 대량 메일 발송
|
||||
router.post('/bulk', (req, res) => mailSendSimpleController.sendBulkMail(req, res));
|
||||
|
||||
// POST /api/mail/send/test-connection - SMTP 연결 테스트
|
||||
router.post('/test-connection', (req, res) => mailSendSimpleController.testConnection(req, res));
|
||||
|
||||
|
||||
@@ -13,10 +13,31 @@ router.get('/statistics', (req, res) => mailSentHistoryController.getStatistics(
|
||||
// GET /api/mail/sent - 발송 이력 목록 조회
|
||||
router.get('/', (req, res) => mailSentHistoryController.getList(req, res));
|
||||
|
||||
// POST /api/mail/sent/draft - 임시 저장 (Draft)
|
||||
router.post('/draft', (req, res) => mailSentHistoryController.saveDraft(req, res));
|
||||
|
||||
// PUT /api/mail/sent/draft/:id - 임시 저장 업데이트
|
||||
router.put('/draft/:id', (req, res) => mailSentHistoryController.updateDraft(req, res));
|
||||
|
||||
// POST /api/mail/sent/bulk/delete - 일괄 삭제
|
||||
router.post('/bulk/delete', (req, res) => mailSentHistoryController.bulkDelete(req, res));
|
||||
|
||||
// POST /api/mail/sent/bulk/permanent-delete - 일괄 영구 삭제
|
||||
router.post('/bulk/permanent-delete', (req, res) => mailSentHistoryController.bulkPermanentDelete(req, res));
|
||||
|
||||
// POST /api/mail/sent/bulk/restore - 일괄 복구
|
||||
router.post('/bulk/restore', (req, res) => mailSentHistoryController.bulkRestore(req, res));
|
||||
|
||||
// POST /api/mail/sent/:id/restore - 메일 복구
|
||||
router.post('/:id/restore', (req, res) => mailSentHistoryController.restoreMail(req, res));
|
||||
|
||||
// DELETE /api/mail/sent/:id/permanent - 메일 영구 삭제
|
||||
router.delete('/:id/permanent', (req, res) => mailSentHistoryController.permanentlyDelete(req, res));
|
||||
|
||||
// GET /api/mail/sent/:id - 특정 발송 이력 상세 조회
|
||||
router.get('/:id', (req, res) => mailSentHistoryController.getById(req, res));
|
||||
|
||||
// DELETE /api/mail/sent/:id - 발송 이력 삭제
|
||||
// DELETE /api/mail/sent/:id - 발송 이력 삭제 (Soft Delete)
|
||||
router.delete('/:id', (req, res) => mailSentHistoryController.deleteById(req, res));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -88,6 +88,9 @@ export class MailReceiveBasicService {
|
||||
port: config.port,
|
||||
tls: config.tls,
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
authTimeout: 30000, // 인증 타임아웃 30초
|
||||
connTimeout: 30000, // 연결 타임아웃 30초
|
||||
keepalive: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -474,29 +477,47 @@ export class MailReceiveBasicService {
|
||||
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
||||
|
||||
const accountAny = account as any;
|
||||
const imapPort = accountAny.imapPort || this.inferImapPort(account.smtpPort);
|
||||
|
||||
const imapConfig: ImapConfig = {
|
||||
user: account.email,
|
||||
password: decryptedPassword,
|
||||
host: accountAny.imapHost || account.smtpHost,
|
||||
port: this.inferImapPort(account.smtpPort, accountAny.imapPort),
|
||||
tls: true,
|
||||
port: imapPort,
|
||||
tls: imapPort === 993, // 993 포트면 TLS 사용
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imap = this.createImapConnection(imapConfig);
|
||||
|
||||
// 타임아웃 설정
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('❌ IMAP 읽음 표시 타임아웃 (30초)');
|
||||
imap.end();
|
||||
reject(new Error("IMAP 연결 타임아웃"));
|
||||
}, 30000);
|
||||
|
||||
imap.once("ready", () => {
|
||||
clearTimeout(timeout);
|
||||
console.log(`🔗 IMAP 연결 성공 - 읽음 표시 시작 (seqno=${seqno})`);
|
||||
|
||||
// false로 변경: 쓰기 가능 모드로 INBOX 열기
|
||||
imap.openBox("INBOX", false, (err: any, box: any) => {
|
||||
if (err) {
|
||||
console.error('❌ INBOX 열기 실패:', err);
|
||||
imap.end();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
console.log(`📬 INBOX 열림 (쓰기 가능 모드)`);
|
||||
|
||||
imap.seq.addFlags(seqno, ["\\Seen"], (flagErr: any) => {
|
||||
imap.end();
|
||||
if (flagErr) {
|
||||
console.error("❌ 읽음 플래그 설정 실패:", flagErr);
|
||||
reject(flagErr);
|
||||
} else {
|
||||
console.log("✅ 읽음 플래그 설정 성공 - seqno:", seqno);
|
||||
resolve({
|
||||
success: true,
|
||||
message: "메일을 읽음으로 표시했습니다.",
|
||||
@@ -507,9 +528,16 @@ export class MailReceiveBasicService {
|
||||
});
|
||||
|
||||
imap.once("error", (imapErr: any) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('❌ IMAP 에러:', imapErr);
|
||||
reject(imapErr);
|
||||
});
|
||||
|
||||
imap.once("end", () => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
|
||||
console.log(`🔌 IMAP 연결 시도 중... (host=${imapConfig.host}, port=${imapConfig.port})`);
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
@@ -774,4 +802,94 @@ export class MailReceiveBasicService {
|
||||
.replace(/_{2,}/g, "_")
|
||||
.substring(0, 200); // 최대 길이 제한
|
||||
}
|
||||
|
||||
/**
|
||||
* IMAP 서버에서 메일 삭제 (휴지통으로 이동)
|
||||
*/
|
||||
async deleteMail(accountId: string, seqno: number): Promise<{ success: boolean; message: string }> {
|
||||
const account = await mailAccountFileService.getAccountById(accountId);
|
||||
|
||||
if (!account) {
|
||||
throw new Error("메일 계정을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 비밀번호 복호화
|
||||
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
||||
|
||||
// IMAP 설정 (타입 캐스팅)
|
||||
const accountAny = account as any;
|
||||
const imapPort = accountAny.imapPort || this.inferImapPort(account.smtpPort);
|
||||
|
||||
const config: ImapConfig = {
|
||||
user: account.smtpUsername || account.email,
|
||||
password: decryptedPassword,
|
||||
host: accountAny.imapHost || account.smtpHost,
|
||||
port: imapPort,
|
||||
tls: imapPort === 993, // 993 포트면 TLS 사용, 143이면 사용 안함
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imap = this.createImapConnection(config);
|
||||
|
||||
// 30초 타임아웃 설정
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('❌ IMAP 메일 삭제 타임아웃 (30초)');
|
||||
imap.end();
|
||||
reject(new Error("IMAP 연결 타임아웃"));
|
||||
}, 30000);
|
||||
|
||||
imap.once("ready", () => {
|
||||
clearTimeout(timeout);
|
||||
console.log(`🔗 IMAP 연결 성공 - 메일 삭제 시작 (seqno=${seqno})`);
|
||||
|
||||
imap.openBox("INBOX", false, (err: any) => {
|
||||
if (err) {
|
||||
console.error('❌ INBOX 열기 실패:', err);
|
||||
imap.end();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
// 메일을 삭제 플래그로 표시 (seq.addFlags 사용)
|
||||
imap.seq.addFlags(seqno, ["\\Deleted"], (flagErr: any) => {
|
||||
if (flagErr) {
|
||||
console.error('❌ 삭제 플래그 추가 실패:', flagErr);
|
||||
imap.end();
|
||||
return reject(flagErr);
|
||||
}
|
||||
|
||||
console.log(`✓ 삭제 플래그 추가 완료 (seqno=${seqno})`);
|
||||
|
||||
// 삭제 플래그가 표시된 메일을 영구 삭제 (실제로는 휴지통으로 이동)
|
||||
imap.expunge((expungeErr: any) => {
|
||||
imap.end();
|
||||
|
||||
if (expungeErr) {
|
||||
console.error('❌ expunge 실패:', expungeErr);
|
||||
return reject(expungeErr);
|
||||
}
|
||||
|
||||
console.log(`🗑️ 메일 삭제 완료: seqno=${seqno}`);
|
||||
resolve({
|
||||
success: true,
|
||||
message: "메일이 삭제되었습니다.",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
imap.once("error", (imapErr: any) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('❌ IMAP 에러:', imapErr);
|
||||
reject(imapErr);
|
||||
});
|
||||
|
||||
imap.once("end", () => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
|
||||
console.log(`🔌 IMAP 연결 시도 중... (host=${config.host}, port=${config.port})`);
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,28 @@ export interface SendMailResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface BulkSendRequest {
|
||||
accountId: string;
|
||||
templateId: string;
|
||||
subject: string;
|
||||
recipients: Array<{
|
||||
email: string;
|
||||
variables: Record<string, string>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface BulkSendResult {
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
results: Array<{
|
||||
email: string;
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
class MailSendSimpleService {
|
||||
/**
|
||||
* 단일 메일 발송 또는 소규모 발송
|
||||
@@ -402,6 +424,72 @@ class MailSendSimpleService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 대량 메일 발송 (배치 처리)
|
||||
*/
|
||||
async sendBulkMail(request: BulkSendRequest): Promise<BulkSendResult> {
|
||||
const results: Array<{
|
||||
email: string;
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
}> = [];
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
console.log(`📧 대량 발송 시작: ${request.recipients.length}명`);
|
||||
|
||||
// 순차 발송 (너무 빠르면 스팸으로 분류될 수 있음)
|
||||
for (const recipient of request.recipients) {
|
||||
try {
|
||||
const result = await this.sendMail({
|
||||
accountId: request.accountId,
|
||||
templateId: request.templateId,
|
||||
to: [recipient.email],
|
||||
subject: request.subject,
|
||||
variables: recipient.variables,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
results.push({
|
||||
email: recipient.email,
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
});
|
||||
} else {
|
||||
failedCount++;
|
||||
results.push({
|
||||
email: recipient.email,
|
||||
success: false,
|
||||
error: result.error || '발송 실패',
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
failedCount++;
|
||||
results.push({
|
||||
email: recipient.email,
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
// 발송 간격 (500ms) - 스팸 방지
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log(`✅ 대량 발송 완료: 성공 ${successCount}, 실패 ${failedCount}`);
|
||||
|
||||
return {
|
||||
total: request.recipients.length,
|
||||
success: successCount,
|
||||
failed: failedCount,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 연결 테스트
|
||||
*/
|
||||
|
||||
@@ -124,6 +124,13 @@ class MailSentHistoryService {
|
||||
// 필터링
|
||||
let filtered = allHistory;
|
||||
|
||||
// 삭제된 메일 필터
|
||||
if (query.onlyDeleted) {
|
||||
filtered = filtered.filter((h) => h.deletedAt);
|
||||
} else if (!query.includeDeleted) {
|
||||
filtered = filtered.filter((h) => !h.deletedAt);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (status !== "all") {
|
||||
filtered = filtered.filter((h) => h.status === status);
|
||||
@@ -209,9 +216,151 @@ class MailSentHistoryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 이력 삭제
|
||||
* 임시 저장 (Draft)
|
||||
*/
|
||||
async saveDraft(
|
||||
data: Partial<SentMailHistory> & { accountId: string }
|
||||
): Promise<SentMailHistory> {
|
||||
console.log("📥 백엔드에서 받은 임시 저장 데이터:", data);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const draft: SentMailHistory = {
|
||||
id: data.id || uuidv4(),
|
||||
accountId: data.accountId,
|
||||
accountName: data.accountName || "",
|
||||
accountEmail: data.accountEmail || "",
|
||||
to: data.to || [],
|
||||
cc: data.cc,
|
||||
bcc: data.bcc,
|
||||
subject: data.subject || "",
|
||||
htmlContent: data.htmlContent || "",
|
||||
templateId: data.templateId,
|
||||
templateName: data.templateName,
|
||||
attachments: data.attachments,
|
||||
sentAt: data.sentAt || now,
|
||||
status: "draft",
|
||||
isDraft: true,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
console.log("💾 저장할 draft 객체:", draft);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
|
||||
}
|
||||
|
||||
const filePath = path.join(SENT_MAIL_DIR, `${draft.id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(draft, null, 2), {
|
||||
encoding: "utf-8",
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("💾 임시 저장:", draft.id);
|
||||
} catch (error) {
|
||||
console.error("임시 저장 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* 임시 저장 업데이트
|
||||
*/
|
||||
async updateDraft(
|
||||
id: string,
|
||||
data: Partial<SentMailHistory>
|
||||
): Promise<SentMailHistory | null> {
|
||||
const existing = await this.getSentMailById(id);
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updated: SentMailHistory = {
|
||||
...existing,
|
||||
...data,
|
||||
id: existing.id,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), {
|
||||
encoding: "utf-8",
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("✏️ 임시 저장 업데이트:", id);
|
||||
return updated;
|
||||
} catch (error) {
|
||||
console.error("임시 저장 업데이트 실패:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 이력 삭제 (Soft Delete)
|
||||
*/
|
||||
async deleteSentMail(id: string): Promise<boolean> {
|
||||
const existing = await this.getSentMailById(id);
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updated: SentMailHistory = {
|
||||
...existing,
|
||||
deletedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), {
|
||||
encoding: "utf-8",
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("🗑️ 메일 삭제 (Soft Delete):", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("메일 삭제 실패:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 복구
|
||||
*/
|
||||
async restoreMail(id: string): Promise<boolean> {
|
||||
const existing = await this.getSentMailById(id);
|
||||
if (!existing || !existing.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updated: SentMailHistory = {
|
||||
...existing,
|
||||
deletedAt: undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), {
|
||||
encoding: "utf-8",
|
||||
mode: 0o644,
|
||||
});
|
||||
|
||||
console.log("♻️ 메일 복구:", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("메일 복구 실패:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 영구 삭제 (Hard Delete)
|
||||
*/
|
||||
async permanentlyDeleteMail(id: string): Promise<boolean> {
|
||||
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
@@ -220,14 +369,57 @@ class MailSentHistoryService {
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
console.log("🗑️ 발송 이력 삭제:", id);
|
||||
console.log("🗑️ 메일 영구 삭제:", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("발송 이력 삭제 실패:", error);
|
||||
console.error("메일 영구 삭제 실패:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 30일 이상 지난 삭제된 메일 자동 영구 삭제
|
||||
*/
|
||||
async cleanupOldDeletedMails(): Promise<number> {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
let deletedCount = 0;
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(SENT_MAIL_DIR)
|
||||
.filter((f) => f.endsWith(".json"));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const filePath = path.join(SENT_MAIL_DIR, file);
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const mail: SentMailHistory = JSON.parse(content);
|
||||
|
||||
if (mail.deletedAt) {
|
||||
const deletedDate = new Date(mail.deletedAt);
|
||||
if (deletedDate < thirtyDaysAgo) {
|
||||
fs.unlinkSync(filePath);
|
||||
deletedCount++;
|
||||
console.log("🗑️ 30일 지난 메일 자동 삭제:", mail.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`파일 처리 실패: ${file}`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("자동 삭제 실패:", error);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
|
||||
@@ -50,11 +50,25 @@ class MailTemplateFileService {
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/app/uploads/mail-templates"
|
||||
: path.join(process.cwd(), "uploads", "mail-templates");
|
||||
this.ensureDirectoryExists();
|
||||
// 동기적으로 디렉토리 생성
|
||||
this.ensureDirectoryExistsSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 디렉토리 생성
|
||||
* 템플릿 디렉토리 생성 (동기)
|
||||
*/
|
||||
private ensureDirectoryExistsSync() {
|
||||
try {
|
||||
const fsSync = require('fs');
|
||||
fsSync.accessSync(this.templatesDir);
|
||||
} catch {
|
||||
const fsSync = require('fs');
|
||||
fsSync.mkdirSync(this.templatesDir, { recursive: true, mode: 0o755 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 디렉토리 생성 (비동기)
|
||||
*/
|
||||
private async ensureDirectoryExists() {
|
||||
try {
|
||||
@@ -75,8 +89,6 @@ class MailTemplateFileService {
|
||||
* 모든 템플릿 목록 조회
|
||||
*/
|
||||
async getAllTemplates(): Promise<MailTemplate[]> {
|
||||
await this.ensureDirectoryExists();
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this.templatesDir);
|
||||
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
||||
@@ -97,6 +109,7 @@ class MailTemplateFileService {
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
} catch (error) {
|
||||
// 디렉토리가 없거나 읽기 실패 시 빈 배열 반환
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,18 @@ export interface SentMailHistory {
|
||||
|
||||
// 발송 정보
|
||||
sentAt: string; // 발송 시간 (ISO 8601)
|
||||
status: 'success' | 'failed'; // 발송 상태
|
||||
status: 'success' | 'failed' | 'draft'; // 발송 상태 (draft 추가)
|
||||
messageId?: string; // SMTP 메시지 ID (성공 시)
|
||||
errorMessage?: string; // 오류 메시지 (실패 시)
|
||||
|
||||
// 발송 결과
|
||||
accepted?: string[]; // 수락된 이메일 주소
|
||||
rejected?: string[]; // 거부된 이메일 주소
|
||||
|
||||
// 임시 저장 및 삭제
|
||||
isDraft?: boolean; // 임시 저장 여부
|
||||
deletedAt?: string; // 삭제 시간 (ISO 8601)
|
||||
updatedAt?: string; // 수정 시간 (ISO 8601)
|
||||
}
|
||||
|
||||
export interface AttachmentInfo {
|
||||
@@ -45,12 +50,14 @@ export interface SentMailListQuery {
|
||||
page?: number; // 페이지 번호 (1부터 시작)
|
||||
limit?: number; // 페이지당 항목 수
|
||||
searchTerm?: string; // 검색어 (제목, 받는사람)
|
||||
status?: 'success' | 'failed' | 'all'; // 필터: 상태
|
||||
status?: 'success' | 'failed' | 'draft' | 'all'; // 필터: 상태 (draft 추가)
|
||||
accountId?: string; // 필터: 발송 계정
|
||||
startDate?: string; // 필터: 시작 날짜 (ISO 8601)
|
||||
endDate?: string; // 필터: 종료 날짜 (ISO 8601)
|
||||
sortBy?: 'sentAt' | 'subject'; // 정렬 기준
|
||||
sortBy?: 'sentAt' | 'subject' | 'updatedAt'; // 정렬 기준 (updatedAt 추가)
|
||||
sortOrder?: 'asc' | 'desc'; // 정렬 순서
|
||||
includeDeleted?: boolean; // 삭제된 메일 포함 여부
|
||||
onlyDeleted?: boolean; // 삭제된 메일만 조회
|
||||
}
|
||||
|
||||
export interface SentMailListResponse {
|
||||
|
||||
Reference in New Issue
Block a user