Phase 11. 이메일 발송 환경 및 자동 발송
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { ACTIVATE_STATUSES } from "@/lib/constants";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { getMailConfig } from "@/lib/mail-config";
|
||||
import {
|
||||
getAccountTypePlayerCount,
|
||||
getAccountTypePrice,
|
||||
} from "@/lib/utils";
|
||||
|
||||
const CTX = "mail";
|
||||
|
||||
function createTransporter() {
|
||||
const cfg = getMailConfig();
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: cfg.smtpHost,
|
||||
port: cfg.smtpPort,
|
||||
secure: cfg.smtpSecure,
|
||||
auth: {
|
||||
user: cfg.smtpUser,
|
||||
pass: cfg.smtpPassword,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type SendMailOptions = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
const isDetailedLog = () => process.env.APP_ENV !== "production";
|
||||
|
||||
async function sendMail(options: SendMailOptions): Promise<boolean> {
|
||||
const cfg = getMailConfig();
|
||||
const detailed = isDetailedLog();
|
||||
|
||||
if (!cfg.sendEnabled) {
|
||||
logger.info(CTX, "send skipped (MAIL_SEND_ENABLED=false)", {
|
||||
to: cfg.overrideTo ?? options.to,
|
||||
subject: options.subject,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const to = cfg.overrideTo ?? options.to;
|
||||
const bcc = cfg.bccAddress || undefined;
|
||||
|
||||
if (detailed) {
|
||||
logger.debug(CTX, "sending", {
|
||||
to,
|
||||
...(bcc && { bcc }),
|
||||
subject: options.subject,
|
||||
smtp: `${cfg.smtpUser}@${cfg.smtpHost}:${cfg.smtpPort}`,
|
||||
...(cfg.overrideTo && { overrideTo: cfg.overrideTo }),
|
||||
});
|
||||
}
|
||||
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const transporter = createTransporter();
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${cfg.fromName}" <${cfg.fromAddress}>`,
|
||||
replyTo: cfg.fromAddress,
|
||||
to,
|
||||
bcc,
|
||||
subject: options.subject,
|
||||
html: options.html,
|
||||
});
|
||||
const duration = Date.now() - t0;
|
||||
|
||||
if (detailed) {
|
||||
logger.info(CTX, "send ok", {
|
||||
to,
|
||||
subject: options.subject,
|
||||
messageId: info.messageId,
|
||||
duration,
|
||||
});
|
||||
} else {
|
||||
logger.info(CTX, "send ok", { duration });
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
const duration = Date.now() - t0;
|
||||
|
||||
if (detailed) {
|
||||
logger.error(CTX, "send failed", {
|
||||
to,
|
||||
subject: options.subject,
|
||||
duration,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
...(err instanceof Error && err.stack && { stack: err.stack }),
|
||||
});
|
||||
} else {
|
||||
logger.error(CTX, "send failed", { duration });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendExtensionDoneEmail(
|
||||
maestroName: string,
|
||||
maestroEmail: string,
|
||||
newAvailableDate: string
|
||||
): Promise<boolean> {
|
||||
const subject = `[초코마에] ${maestroName} - 마에스트로 계정 유효기간 연장 완료`;
|
||||
const html = `안녕하세요.<br/>
|
||||
[초코마에] 마에스트로 계정의 유효기간 연장을 신청해 주셔서 감사합니다.<br/>
|
||||
<br/>
|
||||
${maestroName} - 마에스트로 계정의 유효기간이 +1년 연장되었습니다.<br/>
|
||||
* 새로운 유효기간 : ${newAvailableDate}<br/>
|
||||
<br/>
|
||||
* 상위 요금제로 업그레이드 하시면, 유효기간이 업그레이드한 날짜로부터 1년으로 갱신됩니다.<br/>
|
||||
* 유효기간이 끝나면, 비활성화된 계정과 그 안에 있는 모든 정보가 6개월 후에 자동 삭제됩니다.<br/>
|
||||
<br/>
|
||||
감사합니다.`;
|
||||
|
||||
return sendMail({ to: maestroEmail, subject, html });
|
||||
}
|
||||
|
||||
export async function sendUpgradeDoneEmail(
|
||||
maestroName: string,
|
||||
maestroEmail: string,
|
||||
registeredActivateStatus: number,
|
||||
registeredAccountType: number,
|
||||
requestedAccountType: number,
|
||||
newAvailableDate: string
|
||||
): Promise<boolean> {
|
||||
const subject = `[초코마에] ${maestroName} - 마에스트로 계정의 요금제 업그레이드 완료 알림`;
|
||||
|
||||
const prevStudentCount = getAccountTypePlayerCount(registeredAccountType);
|
||||
const upgradeStudentCount = getAccountTypePlayerCount(requestedAccountType);
|
||||
const upgradePrice = getAccountTypePrice(requestedAccountType).toLocaleString("ko-KR");
|
||||
|
||||
let upgradeFromText: string;
|
||||
if (registeredActivateStatus === ACTIVATE_STATUSES.TRIAL) {
|
||||
upgradeFromText = `${prevStudentCount}명 (0원 : 무료 체험 기간)`;
|
||||
} else {
|
||||
const prevPrice = getAccountTypePrice(registeredAccountType).toLocaleString("ko-KR");
|
||||
upgradeFromText = `${prevStudentCount}명 (${prevPrice}원)`;
|
||||
}
|
||||
|
||||
const html = `안녕하세요.<br/>
|
||||
[초코마에] 마에스트로 계정 요금제 업그레이드를 신청해주셔서 감사합니다.<br/>
|
||||
<br/>
|
||||
${maestroName} - 마에스트로 계정의 요금제가 [ ${upgradeFromText} ] 에서 [ ${upgradeStudentCount}명 (${upgradePrice}원) ] 으로 업그레이드 되었습니다.<br/>
|
||||
<br/>
|
||||
* 마에스트로 계정 유효기간이 오늘부터 1년으로 설정되었습니다.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
등록하신 마에스트로 계정 정보는 마에스트로 로그인 후, [계정 정보 수정] 화면에서 변경이 가능합니다.<br/>
|
||||
(변경 가능 정보 : 마에스트로 아이디, 이메일, 암호)<br/>
|
||||
<br/>
|
||||
감사합니다.`;
|
||||
|
||||
return sendMail({ to: maestroEmail, subject, html });
|
||||
}
|
||||
Reference in New Issue
Block a user