From 8265d89deb38bdf0970780a088173e4a61a4edc6 Mon Sep 17 00:00:00 2001 From: JavaNo0b <98101954+JavaNo0b@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:15:12 +0900 Subject: [PATCH 1/2] feat: add sendEmailToAll --- src/services/sendEmailToAll.js | 144 +++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/services/sendEmailToAll.js diff --git a/src/services/sendEmailToAll.js b/src/services/sendEmailToAll.js new file mode 100644 index 0000000..36b3000 --- /dev/null +++ b/src/services/sendEmailToAll.js @@ -0,0 +1,144 @@ +import AWS from 'aws-sdk'; +import { decryptData, serviceLogger } from '@frolog/common-utils'; +import { handleSqlError } from '@frolog/express-api-server'; +import { User, Review, Memo } from '@frolog/models'; +import { EMAIL_SECRET } from '../common/constants.js'; + +const ses = new AWS.SES({ apiVersion: '2010-12-01' }); + +/** + * 모든 사용자에게 이메일 발송. + * @param {{ is_admin: boolean, id: string }} user 인증 정보. + * @returns {Promise} 응답 DTO. + */ +export default async function sendEmailToAll(user) { + // (1) 관리자 권한 체크 + if (!user.is_admin) { + return { + result: false, + message: 'Access denied. Admin permission required.', + }; + } + + try { + // (2) 모든 사용자 조회 + const allUsers = await User.findAll({ + where: { + deleted_at: null, // 삭제되지 않은 사용자만 + }, + }).catch(handleSqlError); + + if (!allUsers || allUsers.length === 0) { + return { + result: false, + message: 'No users found.', + }; + } + + // (3) 각 사용자별로 리뷰와 메모 조회 및 이메일 발송 + let successCount = 0; + let failureCount = 0; + + for (const targetUser of allUsers) { + try { + // (3.1) 해당 사용자의 리뷰와 메모 데이터 조회 + const [reviews, memos, firstMemos] = await Promise.all([ + Review.findAll({ + where: { + writer_id: targetUser.user_id, + deleted_at: null, + }, + order: [['created_at', 'DESC']], // 최신순 정렬 + }).catch(handleSqlError), + Memo.findAll({ + where: { + writer_id: targetUser.user_id, + deleted_at: null, + is_first: false, // 첫 메모가 아닌 것만 + }, + order: [['created_at', 'DESC']], // 최신순 정렬 + }).catch(handleSqlError), + Memo.findAll({ + where: { + writer_id: targetUser.user_id, + deleted_at: null, + is_first: true, // 첫 메모만 + }, + order: [['created_at', 'DESC']], // 최신순 정렬 + }).catch(handleSqlError), + ]); + + // (3.2) 리뷰와 메모가 모두 없는 사용자 처리 + if ( + (!reviews || reviews.length === 0) && + (!memos || memos.length === 0) && + (!firstMemos || firstMemos.length === 0) + ) { + // TODO: 리뷰와 메모가 모두 없는 사용자에 대한 별도 처리 로직 + // 기본 이메일만 발송 + } + + // (3.3) 이메일 내용 생성 + // TODO: 이메일 내용 생성 로직 구현 + // - reviews, memos, firstMemos 데이터를 활용한 개인화된 이메일 내용 생성 + // - 리뷰, 메모, 첫 메모를 구분하여 csv 생성 + + const email = decryptData( + targetUser.email, + EMAIL_SECRET, + ).toString('utf8'); + + // (3.4) 이메일 발송 + await ses + .sendEmail({ + Destination: { + ToAddresses: [email], + }, + Message: { + Body: { + Text: { + Charset: 'UTF-8', + Data: '이메일 내용', // TODO: 실제 이메일 내용으로 교체 + }, + }, + Subject: { + Charset: 'UTF-8', + Data: '[프롤로그] ~~', // TODO: 실제 이메일 제목으로 교체 + }, + }, + Source: 'no-reply@frolog.kr', + }) + .promise(); + + successCount++; + } catch (error) { + failureCount++; + serviceLogger.warn({ + type: 'email', + event: 'email_send_failed', + message: 'Failed to send email to user', + user_id: targetUser.user_id, + error: error.message, + }); + // 개별 사용자 이메일 발송 실패는 전체 프로세스를 중단시키지 않음 + } + } + + return { + result: true, + message: `이메일 발송 완료: 성공 ${successCount}명, 실패 ${failureCount}명`, + }; + } catch (error) { + serviceLogger.error({ + type: 'email', + event: 'send_email_to_all_failed', + message: 'Failed to send emails to all users', + error: error.message, + }); + + return { + result: false, + message: 'Failed to send emails to all users.', + }; + } +} From bdf37766ece428108a7ffc5b46d38959af635bba Mon Sep 17 00:00:00 2001 From: JavaNo0b <98101954+JavaNo0b@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:17:17 +0900 Subject: [PATCH 2/2] chore: update frolog-api(v1.4.1) --- package-lock.json | 8 ++++---- package.json | 2 +- src/index.js | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index df1172c..1ae1011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@frolog/common-utils": "^1.0.5", "@frolog/express-api-server": "^1.0.8", - "@frolog/frolog-api": "^1.3.3", + "@frolog/frolog-api": "^1.4.1", "@frolog/models": "^1.3.2", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", @@ -1343,9 +1343,9 @@ } }, "node_modules/@frolog/frolog-api": { - "version": "1.3.3", - "resolved": "http://dev.frolog.kr/verdaccio/@frolog/frolog-api/-/frolog-api-1.3.3.tgz", - "integrity": "sha512-G4lGFBslrSNZgobSZA1FMZ6KVD2kGlp6payTrTgBQywnmQRskhtRfzLQhGOTGmes+C4uMgoPdia27k5Nk7LLeg==", + "version": "1.4.1", + "resolved": "http://dev.frolog.kr/verdaccio/@frolog/frolog-api/-/frolog-api-1.4.1.tgz", + "integrity": "sha512-mbfREM7MoX5QnjxFtCIND2Vl15+nH5tp7yZ16gfH1P4eRwduhfXGpkxJZtxPvjQtH3/bWaiDSrGWB/fJ0IkFSA==", "license": "UNLICENSED", "dependencies": { "ajv": "^8.16.0" diff --git a/package.json b/package.json index 5912951..2c5e034 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dependencies": { "@frolog/common-utils": "^1.0.5", "@frolog/express-api-server": "^1.0.8", - "@frolog/frolog-api": "^1.3.3", + "@frolog/frolog-api": "^1.4.1", "@frolog/models": "^1.3.2", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", diff --git a/src/index.js b/src/index.js index 24aa4ef..c96ee4a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import { ApiServer } from '@frolog/express-api-server'; import { + SendEmailToAll, DeleteUser, EditUser, GetUser, @@ -11,6 +12,7 @@ import { SuspendUser, UnsubscribeMailing, } from '@frolog/frolog-api'; +import sendEmailToAll from './services/sendEmailToAll.js'; import postUser from './services/postUser.js'; import editUser from './services/editUser.js'; import searchUser from './services/searchUser.js'; @@ -26,6 +28,7 @@ import sendEmailBulk from './services/sendEmailBulk.js'; const app = new ApiServer(); // API 엔드포인트 연결 +app.addHandler(SendEmailToAll, sendEmailToAll, 'admin', 'info'); app.addHandler(SearchUser, searchUser, 'admin', 'verbose'); app.addHandler(GetUser, getUser, 'admin', 'verbose'); app.addHandler(PostUser, postUser, 'admin', 'info');