diff --git a/backend-node/data/mail-sent/1e492bb1-d069-4242-8cbf-9829b8f6c7e6.json b/backend-node/data/mail-sent/1e492bb1-d069-4242-8cbf-9829b8f6c7e6.json new file mode 100644 index 00000000..eccdc063 --- /dev/null +++ b/backend-node/data/mail-sent/1e492bb1-d069-4242-8cbf-9829b8f6c7e6.json @@ -0,0 +1,29 @@ +{ + "id": "1e492bb1-d069-4242-8cbf-9829b8f6c7e6", + "sentAt": "2025-10-13T01:08:34.764Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "제목 없음", + "htmlContent": "\n\n\n\n \n \n\n\n \n \n \n \n
\n

ㄴㅇㄹㄴㅇㄹ

\n ㄴㅇㄹ버튼\n
\n \"\"\n

ㄴㅇㄹ

ㄴㅇㄹ

\n
\n\n
\n \r\n
\r\n

ㄴㅇㄹ

\r\n
\r\n \n
\n \n\n", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "attachments": [ + { + "filename": "스크린샷 2025-10-13 오전 10.00.06.png", + "originalName": "스크린샷 2025-10-13 오전 10.00.06.png", + "size": 0, + "path": "/app/uploads/mail-attachments/1760317712416-622369845.png", + "mimetype": "image/png" + } + ], + "status": "success", + "messageId": "", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/34f7f149-ac97-442e-b595-02c990082f86.json b/backend-node/data/mail-sent/34f7f149-ac97-442e-b595-02c990082f86.json new file mode 100644 index 00000000..46b0b1b8 --- /dev/null +++ b/backend-node/data/mail-sent/34f7f149-ac97-442e-b595-02c990082f86.json @@ -0,0 +1,29 @@ +{ + "id": "34f7f149-ac97-442e-b595-02c990082f86", + "sentAt": "2025-10-13T01:04:08.560Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "제목 없음", + "htmlContent": "\n\n\n\n \n \n\n\n \n \n \n \n
\n

텍스트 영역 1

\n 버튼1\n
\n \"\"\n

텍스트 영역2

텍스트 영역3

\n
\n\n
\n \r\n
\r\n

선택메시지 영역

\r\n
\r\n \n
\n \n\n", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "attachments": [ + { + "filename": "한글.txt", + "originalName": "한글.txt", + "size": 0, + "path": "/app/uploads/mail-attachments/1760317447824-27488793.txt", + "mimetype": "text/plain" + } + ], + "status": "success", + "messageId": "<1d7caa77-12f1-a791-a230-162826cf03ea@wace.me>", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/3f72cbab-b60e-45e7-ac8d-7e441bc2b900.json b/backend-node/data/mail-sent/3f72cbab-b60e-45e7-ac8d-7e441bc2b900.json new file mode 100644 index 00000000..05eb18c2 --- /dev/null +++ b/backend-node/data/mail-sent/3f72cbab-b60e-45e7-ac8d-7e441bc2b900.json @@ -0,0 +1,29 @@ +{ + "id": "3f72cbab-b60e-45e7-ac8d-7e441bc2b900", + "sentAt": "2025-10-13T01:34:19.363Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "테스트 템플릿이에용22", + "htmlContent": "\n\n\n\n \n \n\n\n \n \n \n \n
\n

안녕안녕하세요 이건 테스트용 템플릿입니다용22

\n \"\"\n

안녕하세용 [222]이안에 뭘 넣어보세용

여기에 뭘 또 입력해보세용[222] 안에 넣어도 돼요

\n
\n\n
\n \r\n
\r\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹ

\r\n
\r\n \n
\n \n\n", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "attachments": [ + { + "filename": "blender study.docx", + "originalName": "blender study.docx", + "size": 0, + "path": "/app/uploads/mail-attachments/1760319257947-827879690.docx", + "mimetype": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + } + ], + "status": "success", + "messageId": "<5b3d9f82-8531-f427-c7f7-9446b4f19da4@wace.me>", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/449d9951-51e8-4e81-ada4-e73aed8ff60e.json b/backend-node/data/mail-sent/449d9951-51e8-4e81-ada4-e73aed8ff60e.json new file mode 100644 index 00000000..29ec634e --- /dev/null +++ b/backend-node/data/mail-sent/449d9951-51e8-4e81-ada4-e73aed8ff60e.json @@ -0,0 +1,20 @@ +{ + "id": "449d9951-51e8-4e81-ada4-e73aed8ff60e", + "sentAt": "2025-10-13T01:29:25.975Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "테스트 템플릿이에용", + "htmlContent": "\n\n\n\n \n \n\n\n \n \n \n \n
\n
안녕안녕하세요 이건 테스트용 템플릿입니다용
\n \"\"\n

안녕하세용 [뭘 넣은 결과 입니당]이안에 뭘 넣어보세용

여기에 뭘 또 입력해보세용[안에 뭘 넣은 결과입니다.] 안에 넣어도 돼요

\n
\n\n\n", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "status": "success", + "messageId": "<5d52accb-777b-b6c2-aab7-1a2f7b7754ab@wace.me>", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/6dd3673a-f510-4ba9-9634-0b391f925230.json b/backend-node/data/mail-sent/6dd3673a-f510-4ba9-9634-0b391f925230.json new file mode 100644 index 00000000..ee094c49 --- /dev/null +++ b/backend-node/data/mail-sent/6dd3673a-f510-4ba9-9634-0b391f925230.json @@ -0,0 +1,29 @@ +{ + "id": "6dd3673a-f510-4ba9-9634-0b391f925230", + "sentAt": "2025-10-13T01:01:55.097Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "테스트용입니당.", + "htmlContent": "\n\n\n\n \n \n\n\n \n \n \n \n
\n \n \n \n \n
\n

텍스트 영역 1

\n 버튼1\n
\n \"\"\n

텍스트 영역2

텍스트 영역3

\n
\n
\n\n
\n \r\n
\r\n

이건 저장이 안되는군

\r\n
\r\n \n
\n \n\n", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "attachments": [ + { + "filename": "한글-분석.txt", + "originalName": "한글-분석.txt", + "size": 0, + "path": "/app/uploads/mail-attachments/1760317313641-761345104.txt", + "mimetype": "text/plain" + } + ], + "status": "success", + "messageId": "", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/9eab902e-f77b-424f-ada4-0ea8709b36bf.json b/backend-node/data/mail-sent/9eab902e-f77b-424f-ada4-0ea8709b36bf.json new file mode 100644 index 00000000..ed2e4b14 --- /dev/null +++ b/backend-node/data/mail-sent/9eab902e-f77b-424f-ada4-0ea8709b36bf.json @@ -0,0 +1,29 @@ +{ + "id": "9eab902e-f77b-424f-ada4-0ea8709b36bf", + "sentAt": "2025-10-13T00:53:55.193Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "제목 없음", + "htmlContent": "

텍스트를 입력하세요...

\n 버튼\n
\n \"\"\n

텍스트를 입력하세요...

텍스트를 입력하세요...

\n
\n \r\n
\r\n

어덯게 나오는지 봅시다 추가메시지 영역이빈다.

\r\n
\r\n \n
\n
", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "attachments": [ + { + "filename": "한글.txt", + "originalName": "한글.txt", + "size": 0, + "path": "/app/uploads/mail-attachments/1760316833254-789302611.txt", + "mimetype": "text/plain" + } + ], + "status": "success", + "messageId": "<3d0bef10-2e58-fd63-b175-c1f499af0102@wace.me>", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/e2801ec2-6219-4c3c-83b4-8a6834569488.json b/backend-node/data/mail-sent/e2801ec2-6219-4c3c-83b4-8a6834569488.json new file mode 100644 index 00000000..1a388699 --- /dev/null +++ b/backend-node/data/mail-sent/e2801ec2-6219-4c3c-83b4-8a6834569488.json @@ -0,0 +1,29 @@ +{ + "id": "e2801ec2-6219-4c3c-83b4-8a6834569488", + "sentAt": "2025-10-13T00:59:46.729Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "제목 없음", + "htmlContent": "

텍스트 영역 1

\n 버튼1\n
\n \"\"\n

텍스트 영역2

텍스트 영역3

\n
\n \r\n
\r\n

추가메시지 영역

\r\n
\r\n \n
\n
", + "templateId": "template-1760315158387", + "templateName": "테스트2", + "attachments": [ + { + "filename": "한글.txt", + "originalName": "한글.txt", + "size": 0, + "path": "/app/uploads/mail-attachments/1760317184642-745285906.txt", + "mimetype": "text/plain" + } + ], + "status": "success", + "messageId": "<1e0abffb-a6cc-8312-d8b4-31c33cb72aa7@wace.me>", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/data/mail-sent/fc26aba3-6b6e-47ba-91e8-609ae25e0e7d.json b/backend-node/data/mail-sent/fc26aba3-6b6e-47ba-91e8-609ae25e0e7d.json new file mode 100644 index 00000000..f64daf8c --- /dev/null +++ b/backend-node/data/mail-sent/fc26aba3-6b6e-47ba-91e8-609ae25e0e7d.json @@ -0,0 +1,29 @@ +{ + "id": "fc26aba3-6b6e-47ba-91e8-609ae25e0e7d", + "sentAt": "2025-10-13T00:21:51.799Z", + "accountId": "account-1759310844272", + "accountName": "이희진", + "accountEmail": "hjlee@wace.me", + "to": [ + "zian9227@naver.com" + ], + "subject": "test용입니다.", + "htmlContent": "\r\n
\r\n

ㅁㄴㅇㄹ

\r\n
\r\n ", + "templateId": "template-1759302346758", + "templateName": "test", + "attachments": [ + { + "filename": "웨이스-임직원-프로파일-이희진.key", + "originalName": "웨이스-임직원-프로파일-이희진.key", + "size": 0, + "path": "/app/uploads/mail-attachments/1760314910154-84512253.key", + "mimetype": "application/x-iwork-keynote-sffkey" + } + ], + "status": "success", + "messageId": "", + "accepted": [ + "zian9227@naver.com" + ], + "rejected": [] +} \ No newline at end of file diff --git a/backend-node/nodemon.json b/backend-node/nodemon.json new file mode 100644 index 00000000..dc43f881 --- /dev/null +++ b/backend-node/nodemon.json @@ -0,0 +1,15 @@ +{ + "watch": ["src"], + "ignore": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "data/**", + "uploads/**", + "logs/**", + "*.log" + ], + "ext": "ts,json", + "exec": "ts-node src/app.ts", + "delay": 2000 +} + diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index c5f793b7..90f26f0f 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -73,8 +73,8 @@ app.use( }) ); app.use(compression()); -app.use(express.json({ limit: "10mb" })); -app.use(express.urlencoded({ extended: true, limit: "10mb" })); +app.use(express.json({ limit: "50mb" })); +app.use(express.urlencoded({ extended: true, limit: "50mb" })); // 정적 파일 서빙 (업로드된 파일들) app.use( @@ -165,6 +165,17 @@ app.use("/api/layouts", layoutRoutes); app.use("/api/mail/accounts", mailAccountFileRoutes); // 파일 기반 계정 app.use("/api/mail/templates-file", mailTemplateFileRoutes); // 파일 기반 템플릿 app.use("/api/mail/send", mailSendSimpleRoutes); // 메일 발송 +// 메일 수신 라우트 디버깅 - 모든 요청 로깅 +app.use("/api/mail/receive", (req, res, next) => { + console.log(`\n🔍 [MAIL RECEIVE REQUEST]`); + console.log(` Method: ${req.method}`); + console.log(` URL: ${req.originalUrl}`); + console.log(` Path: ${req.path}`); + console.log(` Base URL: ${req.baseUrl}`); + console.log(` Params: ${JSON.stringify(req.params)}`); + console.log(` Query: ${JSON.stringify(req.query)}`); + next(); +}); app.use("/api/mail/receive", mailReceiveBasicRoutes); // 메일 수신 app.use("/api/mail/sent", mailSentHistoryRoutes); // 발송 이력 app.use("/api/screen", screenStandardRoutes); diff --git a/backend-node/src/controllers/mailReceiveBasicController.ts b/backend-node/src/controllers/mailReceiveBasicController.ts index ad8b5efa..7722840d 100644 --- a/backend-node/src/controllers/mailReceiveBasicController.ts +++ b/backend-node/src/controllers/mailReceiveBasicController.ts @@ -18,6 +18,12 @@ export class MailReceiveBasicController { */ async getMailList(req: Request, res: Response) { try { + console.log('📬 메일 목록 조회 요청:', { + params: req.params, + path: req.path, + originalUrl: req.originalUrl + }); + const { accountId } = req.params; const limit = parseInt(req.query.limit as string) || 50; @@ -43,6 +49,12 @@ export class MailReceiveBasicController { */ async getMailDetail(req: Request, res: Response) { try { + console.log('🔍 메일 상세 조회 요청:', { + params: req.params, + path: req.path, + originalUrl: req.originalUrl + }); + const { accountId, seqno } = req.params; const seqnoNumber = parseInt(seqno, 10); @@ -109,29 +121,39 @@ export class MailReceiveBasicController { */ async downloadAttachment(req: Request, res: Response) { try { + console.log('📎🎯 컨트롤러 downloadAttachment 진입'); const { accountId, seqno, index } = req.params; + console.log(`📎 파라미터: accountId=${accountId}, seqno=${seqno}, index=${index}`); + const seqnoNumber = parseInt(seqno, 10); const indexNumber = parseInt(index, 10); if (isNaN(seqnoNumber) || isNaN(indexNumber)) { + console.log('❌ 유효하지 않은 파라미터'); return res.status(400).json({ success: false, message: '유효하지 않은 파라미터입니다.', }); } + console.log('📎 서비스 호출 시작...'); const result = await this.mailReceiveService.downloadAttachment( accountId, seqnoNumber, indexNumber ); + console.log(`📎 서비스 호출 완료: result=${result ? '있음' : '없음'}`); if (!result) { + console.log('❌ 첨부파일을 찾을 수 없음'); return res.status(404).json({ success: false, message: '첨부파일을 찾을 수 없습니다.', }); } + + console.log(`📎 파일 다운로드 시작: ${result.filename}`); + console.log(`📎 파일 경로: ${result.filePath}`); // 파일 다운로드 res.download(result.filePath, result.filename, (err) => { @@ -173,5 +195,27 @@ export class MailReceiveBasicController { }); } } + + /** + * GET /api/mail/receive/today-count + * 오늘 수신 메일 수 조회 + */ + async getTodayReceivedCount(req: Request, res: Response) { + try { + const { accountId } = req.query; + const count = await this.mailReceiveService.getTodayReceivedCount(accountId as string); + + return res.json({ + success: true, + data: { count } + }); + } catch (error: unknown) { + console.error('오늘 수신 메일 수 조회 실패:', error); + return res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : '오늘 수신 메일 수 조회에 실패했습니다.' + }); + } + } } diff --git a/backend-node/src/controllers/mailSendSimpleController.ts b/backend-node/src/controllers/mailSendSimpleController.ts index dc92f7cf..de8610b7 100644 --- a/backend-node/src/controllers/mailSendSimpleController.ts +++ b/backend-node/src/controllers/mailSendSimpleController.ts @@ -19,6 +19,9 @@ export class MailSendSimpleController { // FormData에서 JSON 문자열 파싱 const accountId = req.body.accountId; const templateId = req.body.templateId; + const modifiedTemplateComponents = req.body.modifiedTemplateComponents + ? JSON.parse(req.body.modifiedTemplateComponents) + : undefined; // 🎯 수정된 템플릿 컴포넌트 const to = req.body.to ? JSON.parse(req.body.to) : []; const cc = req.body.cc ? JSON.parse(req.body.cc) : undefined; const bcc = req.body.bcc ? JSON.parse(req.body.bcc) : undefined; @@ -90,6 +93,7 @@ export class MailSendSimpleController { const result = await mailSendSimpleService.sendMail({ accountId, templateId, + modifiedTemplateComponents, // 🎯 수정된 템플릿 컴포넌트 전달 to, cc, bcc, diff --git a/backend-node/src/routes/mailReceiveBasicRoutes.ts b/backend-node/src/routes/mailReceiveBasicRoutes.ts index d21df689..d40c4629 100644 --- a/backend-node/src/routes/mailReceiveBasicRoutes.ts +++ b/backend-node/src/routes/mailReceiveBasicRoutes.ts @@ -12,20 +12,29 @@ const router = express.Router(); router.use(authenticateToken); const controller = new MailReceiveBasicController(); -// 메일 목록 조회 -router.get('/:accountId', (req, res) => controller.getMailList(req, res)); +// 오늘 수신 메일 수 조회 (통계) - 가장 먼저 정의 (가장 구체적) +router.get('/today-count', (req, res) => controller.getTodayReceivedCount(req, res)); -// 메일 상세 조회 -router.get('/:accountId/:seqno', (req, res) => controller.getMailDetail(req, res)); +// 첨부파일 다운로드 - 매우 구체적인 경로 +router.get('/:accountId/:seqno/attachment/:index', (req, res) => { + console.log(`📎 첨부파일 라우트 핸들러 진입!`); + console.log(` accountId: ${req.params.accountId}`); + console.log(` seqno: ${req.params.seqno}`); + console.log(` index: ${req.params.index}`); + controller.downloadAttachment(req, res); +}); -// 첨부파일 다운로드 (상세 조회보다 먼저 정의해야 함) -router.get('/:accountId/:seqno/attachment/:index', (req, res) => controller.downloadAttachment(req, res)); - -// 메일 읽음 표시 +// 메일 읽음 표시 - 구체적인 경로 router.post('/:accountId/:seqno/mark-read', (req, res) => controller.markAsRead(req, res)); -// IMAP 연결 테스트 +// 메일 상세 조회 - /:accountId보다 먼저 정의해야 함 +router.get('/:accountId/:seqno', (req, res) => controller.getMailDetail(req, res)); + +// IMAP 연결 테스트 - /:accountId보다 먼저 정의해야 함 router.post('/:accountId/test-imap', (req, res) => controller.testImapConnection(req, res)); +// 메일 목록 조회 - 가장 마지막에 정의 (가장 일반적) +router.get('/:accountId', (req, res) => controller.getMailList(req, res)); + export default router; diff --git a/backend-node/src/services/mailReceiveBasicService.ts b/backend-node/src/services/mailReceiveBasicService.ts index a2ccaa72..2c1112a1 100644 --- a/backend-node/src/services/mailReceiveBasicService.ts +++ b/backend-node/src/services/mailReceiveBasicService.ts @@ -109,7 +109,7 @@ export class MailReceiveBasicService { tls: true, }; - console.log(`📧 IMAP 연결 시도 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, 이메일: ${imapConfig.user}`); + // console.log(`📧 IMAP 연결 시도 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, 이메일: ${imapConfig.user}`); return new Promise((resolve, reject) => { const imap = this.createImapConnection(imapConfig); @@ -117,26 +117,26 @@ export class MailReceiveBasicService { // 30초 타임아웃 설정 const timeout = setTimeout(() => { - console.error('❌ IMAP 연결 타임아웃 (30초)'); + // console.error('❌ IMAP 연결 타임아웃 (30초)'); imap.end(); reject(new Error('IMAP 연결 타임아웃')); }, 30000); imap.once('ready', () => { - console.log('✅ IMAP 연결 성공! INBOX 열기 시도...'); + // console.log('✅ IMAP 연결 성공! INBOX 열기 시도...'); clearTimeout(timeout); imap.openBox('INBOX', true, (err: any, box: any) => { if (err) { - console.error('❌ INBOX 열기 실패:', err); + // console.error('❌ INBOX 열기 실패:', err); imap.end(); return reject(err); } - console.log(`📬 INBOX 열림 - 전체 메일 수: ${box.messages.total}`); + // console.log(`📬 INBOX 열림 - 전체 메일 수: ${box.messages.total}`); const totalMessages = box.messages.total; if (totalMessages === 0) { - console.log('📭 메일함이 비어있습니다'); + // console.log('📭 메일함이 비어있습니다'); imap.end(); return resolve([]); } @@ -145,19 +145,19 @@ export class MailReceiveBasicService { const start = Math.max(1, totalMessages - limit + 1); const end = totalMessages; - console.log(`📨 메일 가져오기 시작 - 범위: ${start}~${end}`); + // console.log(`📨 메일 가져오기 시작 - 범위: ${start}~${end}`); const fetch = imap.seq.fetch(`${start}:${end}`, { bodies: ['HEADER', 'TEXT'], struct: true, }); - console.log(`📦 fetch 객체 생성 완료`); + // console.log(`📦 fetch 객체 생성 완료`); let processedCount = 0; const totalToProcess = end - start + 1; fetch.on('message', (msg: any, seqno: any) => { - console.log(`📬 메일 #${seqno} 처리 시작`); + // console.log(`📬 메일 #${seqno} 처리 시작`); let header: string = ''; let body: string = ''; let attributes: any = null; @@ -207,10 +207,10 @@ export class MailReceiveBasicService { }; mails.push(mail); - console.log(`✓ 메일 #${seqno} 파싱 완료 (${mails.length}/${totalToProcess})`); + // console.log(`✓ 메일 #${seqno} 파싱 완료 (${mails.length}/${totalToProcess})`); processedCount++; } catch (parseError) { - console.error(`메일 #${seqno} 파싱 오류:`, parseError); + // console.error(`메일 #${seqno} 파싱 오류:`, parseError); processedCount++; } } @@ -219,24 +219,24 @@ export class MailReceiveBasicService { }); fetch.once('error', (fetchErr: any) => { - console.error('❌ 메일 fetch 에러:', fetchErr); + // console.error('❌ 메일 fetch 에러:', fetchErr); imap.end(); reject(fetchErr); }); fetch.once('end', () => { - console.log(`📭 fetch 종료 - 처리 완료 대기 중... (현재: ${mails.length}개)`); + // console.log(`📭 fetch 종료 - 처리 완료 대기 중... (현재: ${mails.length}개)`); // 모든 메일 처리가 완료될 때까지 대기 const checkComplete = setInterval(() => { - console.log(`⏳ 대기 중 - 처리됨: ${processedCount}/${totalToProcess}, 메일: ${mails.length}개`); + // console.log(`⏳ 대기 중 - 처리됨: ${processedCount}/${totalToProcess}, 메일: ${mails.length}개`); if (processedCount >= totalToProcess) { clearInterval(checkComplete); - console.log(`✅ 메일 가져오기 완료 - 총 ${mails.length}개`); + // console.log(`✅ 메일 가져오기 완료 - 총 ${mails.length}개`); imap.end(); // 최신 메일이 위로 오도록 정렬 mails.sort((a, b) => b.date.getTime() - a.date.getTime()); - console.log(`📤 메일 목록 반환: ${mails.length}개`); + // console.log(`📤 메일 목록 반환: ${mails.length}개`); resolve(mails); } }, 100); @@ -244,7 +244,7 @@ export class MailReceiveBasicService { // 최대 10초 대기 setTimeout(() => { clearInterval(checkComplete); - console.log(`⚠️ 타임아웃 - 부분 반환: ${mails.length}/${totalToProcess}개`); + // console.log(`⚠️ 타임아웃 - 부분 반환: ${mails.length}/${totalToProcess}개`); imap.end(); mails.sort((a, b) => b.date.getTime() - a.date.getTime()); resolve(mails); @@ -254,16 +254,16 @@ export class MailReceiveBasicService { }); imap.once('error', (imapErr: any) => { - console.error('❌ IMAP 연결 에러:', imapErr.message || imapErr); + // console.error('❌ IMAP 연결 에러:', imapErr.message || imapErr); clearTimeout(timeout); reject(imapErr); }); imap.once('end', () => { - console.log('🔌 IMAP 연결 종료'); + // console.log('🔌 IMAP 연결 종료'); }); - console.log('🔗 IMAP.connect() 호출...'); + // console.log('🔗 IMAP.connect() 호출...'); imap.connect(); }); } @@ -311,22 +311,36 @@ export class MailReceiveBasicService { return reject(err); } + console.log(`📬 INBOX 정보 - 전체 메일: ${box.messages.total}, 요청한 seqno: ${seqno}`); + + if (seqno > box.messages.total || seqno < 1) { + console.error(`❌ 유효하지 않은 seqno: ${seqno} (메일 총 개수: ${box.messages.total})`); + imap.end(); + return resolve(null); + } + const fetch = imap.seq.fetch(`${seqno}:${seqno}`, { bodies: '', struct: true, }); let mailDetail: MailDetail | null = null; + let parsingComplete = false; fetch.on('message', (msg: any, seqnum: any) => { + console.log(`📨 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`); + msg.on('body', (stream: any, info: any) => { + console.log(`📝 메일 본문 스트림 시작 - which: ${info.which}`); let buffer = ''; stream.on('data', (chunk: any) => { buffer += chunk.toString('utf8'); }); stream.once('end', async () => { + console.log(`✅ 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`); try { const parsed = await simpleParser(buffer); + console.log(`✅ 메일 파싱 완료 - 제목: ${parsed.subject}`); const fromAddress = Array.isArray(parsed.from) ? parsed.from[0] : parsed.from; const toAddress = Array.isArray(parsed.to) ? parsed.to[0] : parsed.to; @@ -353,21 +367,48 @@ export class MailReceiveBasicService { size: att.size || 0, })), }; + parsingComplete = true; } catch (parseError) { console.error('메일 파싱 오류:', parseError); + parsingComplete = true; } }); }); + + // msg 전체가 처리되었을 때 이벤트 + msg.once('end', () => { + console.log(`📮 메일 메시지 처리 완료 - seqnum: ${seqnum}`); + }); }); fetch.once('error', (fetchErr: any) => { + console.error(`❌ Fetch 에러:`, fetchErr); imap.end(); reject(fetchErr); }); fetch.once('end', () => { - imap.end(); - resolve(mailDetail); + console.log(`🏁 Fetch 종료 - parsingComplete: ${parsingComplete}`); + + // 비동기 파싱이 완료될 때까지 대기 + const waitForParsing = setInterval(() => { + if (parsingComplete) { + clearInterval(waitForParsing); + console.log(`✅ 파싱 완료 대기 종료 - mailDetail이 ${mailDetail ? '존재함' : 'null'}`); + imap.end(); + resolve(mailDetail); + } + }, 10); // 10ms마다 체크 + + // 타임아웃 설정 (10초) + setTimeout(() => { + if (!parsingComplete) { + clearInterval(waitForParsing); + console.error('❌ 파싱 타임아웃'); + imap.end(); + resolve(mailDetail); // 타임아웃 시에도 현재 상태 반환 + } + }, 10000); }); }); }); @@ -492,6 +533,43 @@ export class MailReceiveBasicService { } } + /** + * 오늘 수신한 메일 수 조회 (통계용) + */ + async getTodayReceivedCount(accountId?: string): Promise { + try { + const accounts = accountId + ? [await mailAccountFileService.getAccountById(accountId)] + : await mailAccountFileService.getAllAccounts(); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + let totalCount = 0; + + for (const account of accounts) { + if (!account) continue; + + try { + const mails = await this.fetchMailList(account.id, 100); + const todayMails = mails.filter(mail => { + const mailDate = new Date(mail.date); + return mailDate >= today; + }); + totalCount += todayMails.length; + } catch (error) { + // 개별 계정 오류는 무시하고 계속 진행 + console.error(`계정 ${account.id} 메일 조회 실패:`, error); + } + } + + return totalCount; + } catch (error) { + console.error('오늘 수신 메일 수 조회 실패:', error); + return 0; + } + } + /** * 첨부파일 다운로드 */ @@ -533,19 +611,26 @@ export class MailReceiveBasicService { }); let attachmentResult: { filePath: string; filename: string; contentType: string } | null = null; + let parsingComplete = false; fetch.on('message', (msg: any, seqnum: any) => { + console.log(`📎 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`); + msg.on('body', (stream: any, info: any) => { + console.log(`📎 메일 본문 스트림 시작`); let buffer = ''; stream.on('data', (chunk: any) => { buffer += chunk.toString('utf8'); }); stream.once('end', async () => { + console.log(`📎 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`); try { const parsed = await simpleParser(buffer); + console.log(`📎 파싱 완료 - 첨부파일 개수: ${parsed.attachments?.length || 0}`); if (parsed.attachments && parsed.attachments[attachmentIndex]) { const attachment = parsed.attachments[attachmentIndex]; + console.log(`📎 첨부파일 발견 (index ${attachmentIndex}): ${attachment.filename}`); // 안전한 파일명 생성 const safeFilename = this.sanitizeFilename( @@ -557,28 +642,51 @@ export class MailReceiveBasicService { // 파일 저장 await fs.writeFile(filePath, attachment.content); + console.log(`📎 파일 저장 완료: ${filePath}`); attachmentResult = { filePath, filename: attachment.filename || 'unnamed', contentType: attachment.contentType || 'application/octet-stream', }; + parsingComplete = true; + } else { + console.log(`❌ 첨부파일 index ${attachmentIndex}를 찾을 수 없음 (총 ${parsed.attachments?.length || 0}개)`); + parsingComplete = true; } } catch (parseError) { console.error('첨부파일 파싱 오류:', parseError); + parsingComplete = true; } }); }); }); fetch.once('error', (fetchErr: any) => { + console.error('❌ fetch 오류:', fetchErr); imap.end(); reject(fetchErr); }); fetch.once('end', () => { - imap.end(); - resolve(attachmentResult); + console.log('📎 fetch.once("end") 호출됨 - 파싱 완료 대기 시작...'); + + // 파싱 완료를 기다림 (최대 5초) + const checkComplete = setInterval(() => { + if (parsingComplete) { + console.log(`✅ 파싱 완료 확인 - attachmentResult: ${attachmentResult ? '있음' : '없음'}`); + clearInterval(checkComplete); + imap.end(); + resolve(attachmentResult); + } + }, 100); + + setTimeout(() => { + clearInterval(checkComplete); + console.log(`⚠️ 타임아웃 - attachmentResult: ${attachmentResult ? '있음' : '없음'}`); + imap.end(); + resolve(attachmentResult); + }, 5000); }); }); }); diff --git a/backend-node/src/services/mailSendSimpleService.ts b/backend-node/src/services/mailSendSimpleService.ts index 5314c004..188e68c8 100644 --- a/backend-node/src/services/mailSendSimpleService.ts +++ b/backend-node/src/services/mailSendSimpleService.ts @@ -12,6 +12,7 @@ import { mailSentHistoryService } from './mailSentHistoryService'; export interface SendMailRequest { accountId: string; templateId?: string; + modifiedTemplateComponents?: any[]; // 🎯 프론트엔드에서 수정된 템플릿 컴포넌트 to: string[]; // 받는 사람 cc?: string[]; // 참조 (Carbon Copy) bcc?: string[]; // 숨은참조 (Blind Carbon Copy) @@ -52,15 +53,29 @@ class MailSendSimpleService { throw new Error('비활성 상태의 계정입니다.'); } - // 3. HTML 생성 (템플릿 또는 커스텀) - htmlContent = request.customHtml || ''; - - if (!htmlContent && request.templateId) { + // 3. HTML 생성 (템플릿 + 추가 메시지 병합) + if (request.templateId) { + // 템플릿 사용 const template = await mailTemplateFileService.getTemplateById(request.templateId); if (!template) { throw new Error('템플릿을 찾을 수 없습니다.'); } + + // 🎯 수정된 컴포넌트가 있으면 덮어쓰기 + if (request.modifiedTemplateComponents && request.modifiedTemplateComponents.length > 0) { + console.log('✏️ 수정된 템플릿 컴포넌트 사용:', request.modifiedTemplateComponents.length); + template.components = request.modifiedTemplateComponents; + } + htmlContent = this.renderTemplate(template, request.variables); + + // 템플릿 + 추가 메시지 병합 + if (request.customHtml && request.customHtml.trim()) { + htmlContent = this.mergeTemplateAndCustomContent(htmlContent, request.customHtml); + } + } else { + // 직접 작성 + htmlContent = request.customHtml || ''; } if (!htmlContent) { @@ -261,13 +276,25 @@ class MailSendSimpleService { } /** - * 템플릿 렌더링 (간단 버전) + * 템플릿 렌더링 (일반 메일 양식) */ private renderTemplate( template: any, variables?: Record ): string { - let html = '
'; + // 일반적인 메일 레이아웃 (전체 너비, 그림자 없음) + let html = ` + + + + + + + + + + + +
+`; template.components.forEach((component: any) => { switch (component.type) { @@ -276,20 +303,23 @@ class MailSendSimpleService { if (variables) { content = this.replaceVariables(content, variables); } - html += `

${content}

`; + // 텍스트는 왼쪽 정렬, 적절한 줄간격 + html += `
${content}
`; break; case 'button': let buttonText = component.text || 'Button'; if (variables) { buttonText = this.replaceVariables(buttonText, variables); } - html += `
- ${buttonText} + // 버튼은 왼쪽 정렬 (text-align 제거) + html += ``; break; case 'image': - html += `
- ${component.alt || ''} + // 이미지는 왼쪽 정렬 + html += `
+ ${component.alt || ''}
`; break; case 'spacer': @@ -298,7 +328,13 @@ class MailSendSimpleService { } }); - html += '
'; + html += ` +
+ + +`; return html; } @@ -320,6 +356,52 @@ class MailSendSimpleService { return result; } + /** + * 템플릿과 추가 메시지 병합 + * 템플릿 HTML의 body 태그 끝 부분에 추가 메시지를 삽입 + */ + private mergeTemplateAndCustomContent(templateHtml: string, customContent: string): string { + // customContent에 HTML 태그가 없으면 기본 스타일 적용 + let formattedCustomContent = customContent; + if (!customContent.includes('<')) { + // 일반 텍스트인 경우 단락으로 변환 + const paragraphs = customContent + .split('\n\n') + .filter((p) => p.trim()) + .map((p) => `

${p.replace(/\n/g, '
')}

`) + .join(''); + + formattedCustomContent = ` +
+ ${paragraphs} +
+ `; + } else { + // 이미 HTML인 경우 구분선만 추가 + formattedCustomContent = ` +
+ ${customContent} +
+ `; + } + + // 또는
태그 앞에 삽입 + if (templateHtml.includes('')) { + return templateHtml.replace('', `${formattedCustomContent}`); + } else if (templateHtml.includes('')) { + // 마지막 앞에 삽입 + const lastDivIndex = templateHtml.lastIndexOf(''); + return ( + templateHtml.substring(0, lastDivIndex) + + formattedCustomContent + + templateHtml.substring(lastDivIndex) + ); + } else { + // 태그가 없으면 단순 결합 + return templateHtml + formattedCustomContent; + } + } + /** * SMTP 연결 테스트 */ diff --git a/backend-node/src/services/mailTemplateFileService.ts b/backend-node/src/services/mailTemplateFileService.ts index cd248193..8b53014a 100644 --- a/backend-node/src/services/mailTemplateFileService.ts +++ b/backend-node/src/services/mailTemplateFileService.ts @@ -141,26 +141,35 @@ class MailTemplateFileService { id: string, data: Partial> ): Promise { - const existing = await this.getTemplateById(id); - if (!existing) { - return null; + try { + const existing = await this.getTemplateById(id); + if (!existing) { + // console.error(`❌ 템플릿을 찾을 수 없음: ${id}`); + return null; + } + + const updated: MailTemplate = { + ...existing, + ...data, + id: existing.id, + createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; + + // console.log(`📝 템플릿 저장 시도: ${id}, 크기: ${JSON.stringify(updated).length} bytes`); + + await fs.writeFile( + this.getTemplatePath(id), + JSON.stringify(updated, null, 2), + 'utf-8' + ); + + // console.log(`✅ 템플릿 저장 성공: ${id}`); + return updated; + } catch (error) { + // console.error(`❌ 템플릿 저장 실패: ${id}`, error); + throw error; // 에러를 컨트롤러로 전달 } - - const updated: MailTemplate = { - ...existing, - ...data, - id: existing.id, - createdAt: existing.createdAt, - updatedAt: new Date().toISOString(), - }; - - await fs.writeFile( - this.getTemplatePath(id), - JSON.stringify(updated, null, 2), - 'utf-8' - ); - - return updated; } /** diff --git a/frontend/app/(dashboard)/admin/system-settings/button-actions/[actionType]/edit/page.tsx b/frontend/app/(dashboard)/admin/system-settings/button-actions/[actionType]/edit/page.tsx deleted file mode 100644 index 306b2e0c..00000000 --- a/frontend/app/(dashboard)/admin/system-settings/button-actions/[actionType]/edit/page.tsx +++ /dev/null @@ -1,513 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; -import { Badge } from "@/components/ui/badge"; -import { toast } from "sonner"; -import { ArrowLeft, Save, RotateCcw, Eye } from "lucide-react"; -import { useButtonActions, type ButtonActionFormData } from "@/hooks/admin/useButtonActions"; -import Link from "next/link"; - -// 기본 카테고리 목록 -const DEFAULT_CATEGORIES = ["crud", "navigation", "utility", "custom"]; - -// 기본 변형 목록 -const DEFAULT_VARIANTS = ["default", "destructive", "outline", "secondary", "ghost", "link"]; - -export default function EditButtonActionPage() { - const params = useParams(); - const router = useRouter(); - const actionType = params.actionType as string; - - const { buttonActions, updateButtonAction, isUpdating, updateError, isLoading } = useButtonActions(); - - const [formData, setFormData] = useState>({}); - const [originalData, setOriginalData] = useState(null); - const [isDataLoaded, setIsDataLoaded] = useState(false); - - const [jsonErrors, setJsonErrors] = useState<{ - validation_rules?: string; - action_config?: string; - }>({}); - - // JSON 문자열 상태 (편집용) - const [jsonStrings, setJsonStrings] = useState({ - validation_rules: "{}", - action_config: "{}", - }); - - // 버튼 액션 데이터 로드 - useEffect(() => { - if (buttonActions && actionType && !isDataLoaded) { - const found = buttonActions.find((ba) => ba.action_type === actionType); - if (found) { - setOriginalData(found); - setFormData({ - action_name: found.action_name, - action_name_eng: found.action_name_eng || "", - description: found.description || "", - category: found.category, - default_text: found.default_text || "", - default_text_eng: found.default_text_eng || "", - default_icon: found.default_icon || "", - default_color: found.default_color || "", - default_variant: found.default_variant || "default", - confirmation_required: found.confirmation_required || false, - confirmation_message: found.confirmation_message || "", - validation_rules: found.validation_rules || {}, - action_config: found.action_config || {}, - sort_order: found.sort_order || 0, - is_active: found.is_active, - }); - setJsonStrings({ - validation_rules: JSON.stringify(found.validation_rules || {}, null, 2), - action_config: JSON.stringify(found.action_config || {}, null, 2), - }); - setIsDataLoaded(true); - } else { - toast.error("버튼 액션을 찾을 수 없습니다."); - router.push("/admin/system-settings/button-actions"); - } - } - }, [buttonActions, actionType, isDataLoaded, router]); - - // 입력값 변경 핸들러 - const handleInputChange = (field: keyof ButtonActionFormData, value: any) => { - setFormData((prev) => ({ - ...prev, - [field]: value, - })); - }; - - // JSON 입력 변경 핸들러 - const handleJsonChange = (field: "validation_rules" | "action_config", value: string) => { - setJsonStrings((prev) => ({ - ...prev, - [field]: value, - })); - - // JSON 파싱 시도 - try { - const parsed = value.trim() ? JSON.parse(value) : {}; - setFormData((prev) => ({ - ...prev, - [field]: parsed, - })); - setJsonErrors((prev) => ({ - ...prev, - [field]: undefined, - })); - } catch (error) { - setJsonErrors((prev) => ({ - ...prev, - [field]: "유효하지 않은 JSON 형식입니다.", - })); - } - }; - - // 폼 유효성 검사 - const validateForm = (): boolean => { - if (!formData.action_name?.trim()) { - toast.error("액션명을 입력해주세요."); - return false; - } - - if (!formData.category?.trim()) { - toast.error("카테고리를 선택해주세요."); - return false; - } - - // JSON 에러가 있는지 확인 - const hasJsonErrors = Object.values(jsonErrors).some((error) => error); - if (hasJsonErrors) { - toast.error("JSON 형식 오류를 수정해주세요."); - return false; - } - - return true; - }; - - // 저장 핸들러 - const handleSave = async () => { - if (!validateForm()) return; - - try { - await updateButtonAction(actionType, formData); - toast.success("버튼 액션이 성공적으로 수정되었습니다."); - router.push(`/admin/system-settings/button-actions/${actionType}`); - } catch (error) { - toast.error(error instanceof Error ? error.message : "수정 중 오류가 발생했습니다."); - } - }; - - // 폼 초기화 (원본 데이터로 되돌리기) - const handleReset = () => { - if (originalData) { - setFormData({ - action_name: originalData.action_name, - action_name_eng: originalData.action_name_eng || "", - description: originalData.description || "", - category: originalData.category, - default_text: originalData.default_text || "", - default_text_eng: originalData.default_text_eng || "", - default_icon: originalData.default_icon || "", - default_color: originalData.default_color || "", - default_variant: originalData.default_variant || "default", - confirmation_required: originalData.confirmation_required || false, - confirmation_message: originalData.confirmation_message || "", - validation_rules: originalData.validation_rules || {}, - action_config: originalData.action_config || {}, - sort_order: originalData.sort_order || 0, - is_active: originalData.is_active, - }); - setJsonStrings({ - validation_rules: JSON.stringify(originalData.validation_rules || {}, null, 2), - action_config: JSON.stringify(originalData.action_config || {}, null, 2), - }); - setJsonErrors({}); - } - }; - - // 로딩 상태 - if (isLoading || !isDataLoaded) { - return ( -
-
버튼 액션 정보를 불러오는 중...
-
- ); - } - - // 버튼 액션을 찾지 못한 경우 - if (!originalData) { - return ( -
-
-
버튼 액션을 찾을 수 없습니다.
- - - -
-
- ); - } - - return ( -
- {/* 헤더 */} -
- - - -
-
-

버튼 액션 편집

- - {actionType} - -
-

{originalData.action_name} 버튼 액션의 정보를 수정합니다.

-
-
- -
- {/* 기본 정보 */} - - - 기본 정보 - 버튼 액션의 기본적인 정보를 수정해주세요. - - - {/* 액션 타입 (읽기 전용) */} -
- - -

액션 타입은 수정할 수 없습니다.

-
- - {/* 액션명 */} -
-
- - handleInputChange("action_name", e.target.value)} - placeholder="예: 저장" - /> -
-
- - handleInputChange("action_name_eng", e.target.value)} - placeholder="예: Save" - /> -
-
- - {/* 카테고리 */} -
- - -
- - {/* 설명 */} -
- -