Skip to content

Commit 4189fbb

Browse files
authored
feat: 이메일 템플릿 구현 (#22)
* feat: 이메일 템플릿 생성 * chore: mjml 패키지 추가 * refactor: 불필요한 코드 파일 삭제 * feat: mjml 형식의 이메일 템플릿 적용 * refactor: 메일 제목 수정 - close #21 * feat: url 대신 code로 에러 조건 확인하도록 수정 * refactor: 공격 설명 수정
1 parent 9f012a6 commit 4189fbb

12 files changed

Lines changed: 1615 additions & 250 deletions

package-lock.json

Lines changed: 1458 additions & 78 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"express": "^5.1.0",
2121
"fluent-ffmpeg": "^2.1.3",
2222
"http-errors": "^2.0.0",
23-
"nodemailer": "^7.0.3"
23+
"nodemailer": "^7.0.3",
24+
"mjml": "^4.15.3"
2425
},
2526
"license": "MIT",
2627
"devDependencies": {

src/app.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@ import createError from "http-errors";
66
import { HTTP_STATUS, MESSAGES } from "./config/constants.js";
77
import env from "./config/env.js";
88
import editRoutes from "./routes/editRoutes.js";
9-
import emailRoutes from "./routes/emailRoutes.js";
109
import videoRoutes from "./routes/videoRoutes.js";
1110

1211
const app = express();
13-
12+
app.use("/images", express.static("public/images"));
1413
app.use(cors());
1514
app.use(express.json());
1615
app.use(express.urlencoded({ extended: true }));
1716

18-
app.use(`${env.API_PREFIX}/`, emailRoutes);
1917
app.use(`${env.API_PREFIX}/video`, videoRoutes);
2018
app.use(`${env.API_PREFIX}/edit`, editRoutes);
2119

src/config/constants.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ const MESSAGES = {
2828
},
2929
};
3030

31+
const CODE = {
32+
SUCCESS: {
33+
SUCCESS_ANALYZE: "SUCCESS_ANALYZE",
34+
},
35+
ERROR: {
36+
FAILED_ANALYZE: "FAILED_ANALYZE",
37+
},
38+
};
39+
3140
const HTTP_STATUS = {
3241
OK: 200,
3342
CREATED: 201,
@@ -52,4 +61,4 @@ const REGEX_PATTERNS = {
5261
EMAIL: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i,
5362
};
5463

55-
export { MESSAGES, HTTP_STATUS, GCS, REQUIRED_FIELDS, REGEX_PATTERNS };
64+
export { MESSAGES, HTTP_STATUS, GCS, REQUIRED_FIELDS, REGEX_PATTERNS, CODE };

src/controllers/emailController.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/routes/emailRoutes.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/services/emailService.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { fileURLToPath } from "url";
44

55
import ejs from "ejs";
66
import createError from "http-errors";
7+
import mjml2html from "mjml";
78
import nodemailer from "nodemailer";
89

9-
import { MESSAGES } from "../config/constants.js";
10+
import { MESSAGES, CODE } from "../config/constants.js";
1011
import env from "../config/env.js";
1112

1213
const filename = fileURLToPath(import.meta.url);
@@ -20,28 +21,34 @@ const transporter = nodemailer.createTransport({
2021
},
2122
});
2223

23-
const readEmailTemplate = async ({ message, url }) => {
24+
const readEmailTemplate = async ({ code, message, url }) => {
2425
try {
25-
if (url === "") {
26-
const filePath = path.join(dirname, "../views/errorTemplate.ejs");
26+
if (code === CODE.ERROR.FAILED_ANALYZE) {
27+
const filePath = path.join(dirname, "../views", "errorTemplate.mjml.ejs");
2728
const template = await fs.readFile(filePath, "utf-8");
2829
const htmlContent = ejs.render(template, { message });
30+
const { html } = mjml2html(htmlContent, {
31+
validationLevel: "strict",
32+
});
2933

30-
return htmlContent;
34+
return html;
3135
}
32-
const filePath = path.join(dirname, "../views/emailTemplate.ejs");
36+
const filePath = path.join(dirname, "../views", "emailTemplate.mjml.ejs");
3337
const template = await fs.readFile(filePath, "utf-8");
34-
const htmlContent = ejs.render(template, { message, url });
38+
const htmlContent = ejs.render(template, { url });
39+
const { html } = mjml2html(htmlContent, {
40+
validationLevel: "strict",
41+
});
3542

36-
return htmlContent;
43+
return html;
3744
} catch (err) {
3845
throw createError.InternalServerError(MESSAGES.ERROR.FAILED_READ_TEMPLATE);
3946
}
4047
};
4148

42-
const sendEmail = async ({ email, message, url }) => {
49+
const sendEmail = async ({ email, code, message, url }) => {
4350
try {
44-
const html = await readEmailTemplate({ message, url });
51+
const html = await readEmailTemplate({ code, message, url });
4552
const mailOptions = {
4653
from: env.email_user,
4754
to: email,

src/utils/rabbitmqService.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ const consumeEmailQueue = async () => {
3535
return;
3636
}
3737

38-
const { email, message, url } = JSON.parse(msg.content.toString());
38+
const { email, code, message, url } = JSON.parse(msg.content.toString());
3939

4040
try {
41-
await sendEmail({ email, message, url });
41+
await sendEmail({ email, code, message, url });
4242
channel.ack(msg);
4343
} catch {
4444
channel.nack(msg, false, false);

src/views/emailTemplate.ejs

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/views/emailTemplate.mjml.ejs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<mjml>
2+
<mj-head>
3+
<mj-font
4+
name="Pretendard"
5+
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"
6+
/>
7+
<mj-attributes>
8+
<mj-all font-family="Pretendard, Arial" color="#404040" />
9+
</mj-attributes>
10+
</mj-head>
11+
<mj-body>
12+
<mj-section
13+
background-color="#ffffff"
14+
border="1px solid #E5E7EB"
15+
border-radius="8px"
16+
padding="20px"
17+
>
18+
<mj-column>
19+
<mj-image
20+
width="90px"
21+
height="40px"
22+
src="https://raw.githubusercontent.com/cholulu99/commandTrackerImage/main/3f4-7420ead14100-removebg-preview.png"
23+
></mj-image>
24+
<mj-spacer height="20px"></mj-spacer>
25+
<mj-text align="center" font-size="16px" line-height="1.5">
26+
command Tracker 서비스를 사용해주셔서 감사합니다.
27+
</mj-text>
28+
<mj-text align="center" font-size="16px" line-height="1.5">
29+
요청하신 캐릭터의 커맨드는 자막으로 보여집니다.
30+
</mj-text>
31+
<mj-divider border-color="#E5E7EB" border-width="1px"></mj-divider>
32+
<mj-text align="center" font-size="16px">
33+
아래는 스트리트파이터 6의 기본 공격 커맨드입니다:
34+
</mj-text>
35+
<mj-text align="center" font-size="14px">
36+
<p>LP: 약펀치</p>
37+
<p>MP: 중간펀치</p>
38+
<p>HP: 강펀치</p>
39+
<p>LK: 약발</p>
40+
<p>MK: 중발</p>
41+
<p>HK: 강발</p>
42+
</mj-text>
43+
<mj-button
44+
align="center"
45+
href="<%= url %>"
46+
background-color="#007BFF"
47+
color="#ffffff"
48+
>
49+
영상 다운로드
50+
</mj-button>
51+
<mj-divider border-color="#E5E7EB" border-width="1px"></mj-divider>
52+
<mj-text align="center" font-size="12px" line-height="1.75">
53+
<a
54+
href="https://www.naver.com"
55+
target="_blank"
56+
rel="noopener noreferrer nofollow"
57+
>웹사이트</a
58+
>
59+
</mj-text>
60+
<mj-text
61+
align="center"
62+
font-family="Pretendard, Arial"
63+
font-size="12px"
64+
line-height="1"
65+
>
66+
© command Tracker. All Rights Reserved.
67+
</mj-text>
68+
</mj-column>
69+
</mj-section>
70+
</mj-body>
71+
</mjml>

0 commit comments

Comments
 (0)