Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiServer } from '@frolog/express-api-server';
import {
SendEmailToAll,
DeleteUser,
EditUser,
GetUser,
Expand All @@ -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';
Expand All @@ -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');
Expand Down
144 changes: 144 additions & 0 deletions src/services/sendEmailToAll.js
Original file line number Diff line number Diff line change
@@ -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<import('@frolog/frolog-api').SendEmailToAllRes>} 응답 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.',
};
}
}
Loading