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: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=your_username
RABBITMQ_PASSWORD=your_password
RABBITMQ_PASSWORD=your_password

# Mail
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your_mail
MAIL_PASSWORD=your_mail_password
6 changes: 6 additions & 0 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ REQUIRED_VARS=(
"RABBITMQ_PORT"
"RABBITMQ_USERNAME"
"RABBITMQ_PASSWORD"

# Mail
"MAIL_HOST"
"MAIL_PORT"
"MAIL_USERNAME"
"MAIL_PASSWORD"
)

MISSING=()
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/ditda/notification/mail/EmailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package ditda.notification.mail;

import java.io.UnsupportedEncodingException;
import java.util.Map;

import org.springframework.boot.mail.autoconfigure.MailProperties;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class EmailSender {

private static final String LOGO_CID = "logoImage";
private static final String LOGO_PATH = "email-images/logo.png";

private static final String FROM_NAME = "DITDA";

private final JavaMailSender mailSender;
private final SpringTemplateEngine templateEngine;
private final MailProperties mailProperties;

public void send(String to, NotificationType type, Map<String, Object> variables)
throws MessagingException {

MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

try {
helper.setFrom(new InternetAddress(mailProperties.getUsername(), FROM_NAME, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 is not supported", e);
}

helper.setTo(to);
helper.setSubject(type.getSubject());
helper.setText(renderTemplate(type.getTemplate(), variables), true);

helper.addInline(LOGO_CID, new ClassPathResource(LOGO_PATH));

mailSender.send(mimeMessage);

log.info("[Email] Email sent. to={}, type={}", MailMasker.mask(to), type);
}

private String renderTemplate(String templateName, Map<String, Object> variables) {
Context context = new Context();
if (variables != null) {
variables.forEach(context::setVariable);
}

return templateEngine.process(templateName, context);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
12 changes: 10 additions & 2 deletions src/main/java/ditda/notification/mail/MailListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class MailListener {

private final EmailSender emailSender;

@RabbitListener(queues = MailRabbitConfig.MAIL_QUEUE)
public void handle(MailMessage message) {
log.info("메일 메세지 수신. type: {}, to: {}", message.type(), message.to());
public void handle(MailMessage message) throws MessagingException {
log.info("[Email] Mail message received. type: {}, to: {}", message.type(), MailMasker.mask(message.to()));

NotificationType type = NotificationType.valueOf(message.type());
emailSender.send(message.to(), type, message.variables());
}
}
26 changes: 26 additions & 0 deletions src/main/java/ditda/notification/mail/MailMasker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ditda.notification.mail;

import org.springframework.util.StringUtils;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MailMasker {

public static String mask(String email) {
if (!StringUtils.hasText(email) || !email.contains("@")) {
return "***";
}

int at = email.indexOf("@");
String local = email.substring(0, at);
String domain = email.substring(at);

if (local.length() <= 2) {
return "****" + domain;
}

return local.substring(0, 2) + "****" + domain;
}
}
81 changes: 81 additions & 0 deletions src/main/java/ditda/notification/mail/NotificationType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package ditda.notification.mail;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum NotificationType {

// 지원 마감 (ApplicationDeadlineClosed)
APPLICATION_REFUND_REQUEST_ADMIN(
"[DITDA] 외주 마감에 따른 환불 처리 요망", "email/admin-refund-request"),
APPLICATION_CANCELLED_INSTRUCTOR(
"[DITDA] 신청하신 외주가 지원자 부족으로 취소되었습니다.", "email/commission-cancelled"),
APPLICATION_SHORTFALL_INSTRUCTOR(
"[DITDA] 신청하신 외주의 모집 인원이 미달되었습니다.", "email/commission-shortfall-instructor"),
APPLICATION_MATCHED_INSTRUCTOR(
"[DITDA] 신청하신 외주의 디자이너 매칭이 완료되었습니다.", "email/commission-matched-instructor"),
APPLICATION_MATCHED_DESIGNER(
"[DITDA] 지원하신 외주의 1차 시안 대상자로 선정되었습니다.", "email/commission-matched-designer"),

// 1차 시안 마감 (FirstDraftDeadlineClosed)
FIRST_DRAFT_REFUND_REQUEST_ADMIN(
"[DITDA] 1차 시안 미제출에 따른 환불 처리 요망", "email/admin-refund-request"),
FIRST_DRAFT_ZERO_INSTRUCTOR(
"[DITDA] 1차 시안 제출이 없어 외주가 취소되었습니다.", "email/first-draft-zero-instructor"),
FIRST_DRAFT_SHORTFALL_INSTRUCTOR(
"[DITDA] 일부 디자이너의 1차 시안이 미제출되었습니다.", "email/first-draft-shortfall-instructor"),
FIRST_DRAFT_MISSED_DESIGNER(
"[DITDA] 1차 시안 기한 초과로 외주 진행이 종료되었습니다.", "email/first-draft-missed-designer"),

// 최종 마감 (FinalDeadlineClosed)
FINAL_CANCELLED_INSTRUCTOR(
"[DITDA] 시안 선택 기한 초과로 외주가 취소되었습니다.", "email/final-cancelled-instructor"),
FINAL_CANCELLED_DESIGNER(
"[DITDA] 강사 미선택으로 외주가 취소되었습니다.", "email/final-cancelled-designer"),

// 전원 1차 시안 제출 (AllFirstDraftsSubmitted)
ALL_FIRST_DRAFTS_SUBMITTED_INSTRUCTOR(
"[DITDA] 모든 1차 시안이 제출되었습니다. 시안을 선택해 주세요.", "email/first-draft-all-submitted-instructor"),

// 시안 선택 (DraftSelected)
DRAFT_SELECTED_DESIGNER(
"[DITDA] 제출하신 1차 시안이 최종 선택되었습니다.", "email/first-draft-selected-designer"),
DRAFT_REJECTED_DESIGNER(
"[DITDA] 1차 시안 선정 결과를 안내해 드립니다.", "email/first-draft-rejected-designer"),

// 외주 최종 확정 (CommissionCompleted)
COMMISSION_FINALIZED_INSTRUCTOR(
"[DITDA] 신청하신 외주가 최종 확정되었습니다.", "email/commission-finalized-instructor"),
COMMISSION_FINALIZED_DESIGNER(
"[DITDA] 작업하신 외주가 최종 확정되었습니다.", "email/commission-finalized-designer"),

// 수정 요청/제출 (RevisionRequested / RevisionSubmitted)
REVISION_REQUESTED_DESIGNER(
"[DITDA] 시안 수정 요청이 도착했습니다. 확인해주세요.", "email/revision-requested-designer"),
REVISION_SUBMITTED_INSTRUCTOR(
"[DITDA] 수정본이 제출되었습니다. 확인해주세요.", "email/revision-submitted-instructor"),

// 정산 요청 (PayoutRequested)
PAYOUT_REQUEST_COMPLETED_ADMIN(
"[DITDA] 외주 최종 확정 - 디자이너 정산 요청", "email/admin-payout-request"),
PAYOUT_REQUEST_CANCELLED_ADMIN(
"[DITDA] 외주 취소 - 제출 디자이너 정산 요청", "email/admin-payout-request"),
PAYOUT_REQUEST_REJECTED_ADMIN(
"[DITDA] 시안 선택 완료 - 미선택 디자이너 정산 요청", "email/admin-payout-request"),

// auth
EMAIL_VERIFICATION(
"[DITDA] 이메일 인증 코드", "email/verification-code"),
DESIGNER_SIGNUP_REVIEW_ADMIN(
"[DITDA] 새 디자이너 가입 검토 요청", "email/designer-signup-notification"),

// payment
DEPOSIT_CONFIRM_REQUEST_ADMIN(
"[DITDA] 외주 입금 확인 요청", "email/deposit-notification");

private final String subject;
private final String template;
}

17 changes: 16 additions & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,20 @@ spring:
initial-interval: 2s
multiplier: 2
max-interval: 10s
max-retries: 3
max-retries: 2
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default-requeue-rejected: false

mail:
host: ${MAIL_HOST}
port: ${MAIL_PORT:587}
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
connectiontimeout: 5000
timeout: 10000
writetimeout: 10000
Binary file added src/main/resources/email-images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 100 additions & 0 deletions src/main/resources/templates/email/admin-payout-request.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<div style="font-family: 'Apple SD Gothic Neo', sans-serif; max-width: 560px; margin: 0 auto; padding: 24px;">

<div style="text-align: center; margin-bottom: 24px;">
<img alt="ditda" src="cid:logoImage" style="height: 48px;">
</div>

<div style="text-align: center;">
<h2 style="color: #874fff;">디자이너 정산 요청</h2>
<p style="color: #666; line-height: 1.6;">
<span th:switch="${reason}">
<span th:case="'FINAL_COMPLETED_AUTO'">외주가 <strong>자동 최종 확정</strong>되어 디자이너 정산이 필요합니다.</span>
<span th:case="'FINAL_COMPLETED_MANUAL'">외주가 <strong>강사 수동 확정</strong>되어 디자이너 정산이 필요합니다.</span>
<span th:case="'FINAL_CANCELLED_BY_DEADLINE'">외주가 <strong>강사 미선택으로 취소</strong>되어 제출 디자이너에 대한 기본금 정산이 필요합니다.</span>
<span th:case="'DRAFT_SELECTION_REJECTED'">강사가 시안을 선택하여 <strong>미선택 디자이너에 대한 기본금 정산</strong>이 필요합니다.</span>
</span><br>
<strong>어드민 Swagger에서 디자이너 계좌 정보를 조회 후 송금</strong>해 주세요.
</p>
</div>

<!-- 정산 안내 -->
<div style="text-align: center;">
<p style="color: #666; line-height: 1.6;">
<strong>📌 정산 안내</strong><br>
계좌 정보는 보안상 메일에 포함되지 않습니다.<br>
어드민 Swagger에서 디자이너 ID로 계좌 정보를 조회하세요.
</p>
</div>

<!-- 외주 정보 -->
<div style="margin-top: 24px; padding: 16px 20px; background: #f5f5f7; border-radius: 8px;">
<p style="margin: 0 0 12px 0; font-weight: 700; color: #222;">📋 외주 정보</p>
<div style="margin: 6px 0;">
<span style="display: inline-block; width: 110px; color: #888; font-size: 14px;">외주 ID</span>
<span style="font-weight: 600;" th:text="${commissionId}"></span>
</div>
<div style="margin: 6px 0;">
<span style="display: inline-block; width: 110px; color: #888; font-size: 14px;">외주명</span>
<span style="font-weight: 600;" th:text="${commissionTitle}"></span>
</div>
<div style="margin: 6px 0;">
<span style="display: inline-block; width: 110px; color: #888; font-size: 14px;">강사</span>
<span style="font-weight: 600;"
th:text="${instructorName} + ' (' + ${instructorEmail} + ')'"></span>
</div>
</div>

<!-- 정산 대상 -->
<div style="margin-top: 16px; padding: 16px 20px; background: #fdf4ff; border-radius: 8px; border-left: 4px solid #874fff;">
<p style="margin: 0 0 12px 0; font-weight: 700; color: #222;">
💰 정산 대상
<span style="color: #888; font-size: 14px; font-weight: 400;"
th:text="'(' + ${payoutCount} + '명)'"></span>
</p>

<div th:each="payout, iter : ${payouts}"
th:style="${iter.index > 0} ? 'margin-top: 12px; padding-top: 12px; border-top: 1px dashed #ddd;' : ''">
<div style="margin: 4px 0;">
<span style="display: inline-block; width: 110px; color: #888; font-size: 14px;">디자이너 ID</span>
<span style="font-weight: 600;" th:text="${payout.designerId}"></span>
</div>
<div style="margin: 4px 0;">
<span style="display: inline-block; width: 110px; color: #888; font-size: 14px;">디자이너</span>
<span style="font-weight: 600;"
th:text="${payout.designerName} + ' (' + ${payout.level} + ')'"></span>
</div>
<div style="margin: 4px 0;">
<span style="display: inline-block; width: 110px; color: #888; font-size: 14px;">정산 금액</span>
<span style="font-weight: 700; color: #874fff;"
th:text="${#numbers.formatInteger(payout.amount, 1, 'COMMA')} + '원'"></span>
</div>
Comment thread
fervovita marked this conversation as resolved.
</div>

<div style="margin-top: 16px; padding-top: 12px; border-top: 2px solid #874fff;">
<div style="margin: 4px 0;">
<span style="display: inline-block; width: 110px; color: #222; font-size: 14px; font-weight: 700;">총 정산 금액</span>
<span style="font-weight: 800; color: #874fff; font-size: 18px;"
th:text="${#numbers.formatInteger(totalAmount, 1, 'COMMA')} + '원'"></span>
</div>
</div>
</div>

<div style="margin-top: 24px; text-align: center;">
<a href="https://api.ditda.kr/swagger-ui/index.html"
style="display: inline-block; padding: 12px 24px; background: #874fff; color: #fff; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 14px;">
어드민 Swagger 바로가기
</a>
</div>

<p style="color: #888; margin-top: 24px; font-size: 13px; text-align: center;">
본 메일은 시스템에 의해 자동 발송된 관리자 전용 알림입니다.
</p>
</div>
</body>
</html>
Loading