From 17b687d4c41db0642e77df22c94dd0ddeece590b Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Fri, 16 Jan 2026 00:49:27 +0900 Subject: [PATCH 01/17] =?UTF-8?q?[SRLT-132]=20feat:=20=EB=B0=B1=EC=98=A4?= =?UTF-8?q?=ED=94=BC=EC=8A=A4=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- .../backoffice/mail/email/SmtpMailSender.java | 46 ++++++++++ .../persistence/BackofficeMailSendLogJpa.java | 18 ++++ .../BackofficeMailSendLogRepository.java | 7 ++ .../BackofficeMailTemplateJpa.java | 31 +++++++ .../BackofficeMailTemplateRepository.java | 7 ++ .../mail/webapi/BackofficeMailController.java | 52 +++++++++++ .../request/BackofficeMailSendRequest.java | 34 ++++++++ .../BackofficeMailTemplateCreateRequest.java | 26 ++++++ .../BackofficeMailSendLogResponse.java | 23 +++++ .../BackofficeMailTemplateResponse.java | 27 ++++++ .../mail/BackofficeMailLogService.java | 55 ++++++++++++ .../mail/BackofficeMailSendService.java | 87 +++++++++++++++++++ .../mail/BackofficeMailTemplateService.java | 77 ++++++++++++++++ .../provided/BackofficeMailLogUseCase.java | 9 ++ .../provided/BackofficeMailSendUseCase.java | 9 ++ .../BackofficeMailTemplateUseCase.java | 15 ++++ .../dto/input/BackofficeMailSendInput.java | 12 +++ .../BackofficeMailSendLogCreateInput.java | 14 +++ .../BackofficeMailTemplateCreateInput.java | 12 +++ .../result/BackofficeMailSendLogResult.java | 14 +++ .../result/BackofficeMailTemplateResult.java | 14 +++ .../BackofficeMailSendLogCommandPort.java | 8 ++ .../BackofficeMailTemplateCommandPort.java | 10 +++ .../BackofficeMailTemplateQueryPort.java | 10 +++ .../mail/required/MailSenderPort.java | 9 ++ .../starlight/bootstrap/SecurityConfig.java | 2 + .../exception/BackofficeErrorType.java | 24 +++++ .../exception/BackofficeException.java | 11 +++ .../mail/BackofficeMailContentType.java | 26 ++++++ .../mail/BackofficeMailSendLog.java | 50 +++++++++++ .../mail/BackofficeMailTemplate.java | 50 +++++++++++ 32 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogJpa.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogRepository.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateJpa.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateRepository.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java create mode 100644 src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailTemplateResponse.java create mode 100644 src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java create mode 100644 src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java create mode 100644 src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailTemplateUseCase.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java create mode 100644 src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java create mode 100644 src/main/java/starlight/application/backoffice/mail/required/BackofficeMailSendLogCommandPort.java create mode 100644 src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateCommandPort.java create mode 100644 src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateQueryPort.java create mode 100644 src/main/java/starlight/application/backoffice/mail/required/MailSenderPort.java create mode 100644 src/main/java/starlight/domain/backoffice/exception/BackofficeErrorType.java create mode 100644 src/main/java/starlight/domain/backoffice/exception/BackofficeException.java create mode 100644 src/main/java/starlight/domain/backoffice/mail/BackofficeMailContentType.java create mode 100644 src/main/java/starlight/domain/backoffice/mail/BackofficeMailSendLog.java create mode 100644 src/main/java/starlight/domain/backoffice/mail/BackofficeMailTemplate.java diff --git a/config b/config index f99b5394..5acba1ec 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit f99b5394463d91b5c31a7907dd48c81e1760ee61 +Subproject commit 5acba1ecc85733f96efaa9c60db314292dca268f diff --git a/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java b/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java new file mode 100644 index 00000000..47e3117d --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java @@ -0,0 +1,46 @@ +package starlight.adapter.backoffice.mail.email; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; +import starlight.application.backoffice.mail.required.MailSenderPort; +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SmtpMailSender implements MailSenderPort { + + private final JavaMailSender javaMailSender; + + @Value("${spring.mail.username}") + private String senderEmail; + + @Override + public void send(BackofficeMailSendInput input, BackofficeMailContentType contentType) { + try { + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(senderEmail); + helper.setTo(input.to().toArray(new String[0])); + helper.setSubject(input.subject()); + + boolean isHtml = contentType == BackofficeMailContentType.HTML; + String body = isHtml ? input.html() : input.text(); + helper.setText(body, isHtml); + + javaMailSender.send(message); + log.info("[MAIL] sent to={} subject={}", input.to(), input.subject()); + } catch (MessagingException e) { + log.error("[MAIL] send failed to={}", input.to(), e); + throw new IllegalArgumentException("메일 전송 실패"); + } + } +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogJpa.java b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogJpa.java new file mode 100644 index 00000000..600e70e1 --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogJpa.java @@ -0,0 +1,18 @@ +package starlight.adapter.backoffice.mail.persistence; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import starlight.application.backoffice.mail.required.BackofficeMailSendLogCommandPort; +import starlight.domain.backoffice.mail.BackofficeMailSendLog; + +@Repository +@RequiredArgsConstructor +public class BackofficeMailSendLogJpa implements BackofficeMailSendLogCommandPort { + + private final BackofficeMailSendLogRepository repository; + + @Override + public BackofficeMailSendLog save(BackofficeMailSendLog log) { + return repository.save(log); + } +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogRepository.java b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogRepository.java new file mode 100644 index 00000000..b8b7dab3 --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailSendLogRepository.java @@ -0,0 +1,7 @@ +package starlight.adapter.backoffice.mail.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import starlight.domain.backoffice.mail.BackofficeMailSendLog; + +public interface BackofficeMailSendLogRepository extends JpaRepository { +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateJpa.java b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateJpa.java new file mode 100644 index 00000000..63e4b42c --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateJpa.java @@ -0,0 +1,31 @@ +package starlight.adapter.backoffice.mail.persistence; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import starlight.application.backoffice.mail.required.BackofficeMailTemplateCommandPort; +import starlight.application.backoffice.mail.required.BackofficeMailTemplateQueryPort; +import starlight.domain.backoffice.mail.BackofficeMailTemplate; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class BackofficeMailTemplateJpa implements BackofficeMailTemplateCommandPort, BackofficeMailTemplateQueryPort { + + private final BackofficeMailTemplateRepository repository; + + @Override + public BackofficeMailTemplate save(BackofficeMailTemplate template) { + return repository.save(template); + } + + @Override + public void deleteById(Long templateId) { + repository.deleteById(templateId); + } + + @Override + public List findAll() { + return repository.findAll(); + } +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateRepository.java b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateRepository.java new file mode 100644 index 00000000..0ecd52aa --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/persistence/BackofficeMailTemplateRepository.java @@ -0,0 +1,7 @@ +package starlight.adapter.backoffice.mail.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import starlight.domain.backoffice.mail.BackofficeMailTemplate; + +public interface BackofficeMailTemplateRepository extends JpaRepository { +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java new file mode 100644 index 00000000..a4d24f89 --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java @@ -0,0 +1,52 @@ +package starlight.adapter.backoffice.mail.webapi; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import starlight.adapter.backoffice.mail.webapi.dto.request.BackofficeMailSendRequest; +import starlight.adapter.backoffice.mail.webapi.dto.response.BackofficeMailSendLogResponse; +import starlight.adapter.backoffice.mail.webapi.dto.request.BackofficeMailTemplateCreateRequest; +import starlight.adapter.backoffice.mail.webapi.dto.response.BackofficeMailTemplateResponse; +import starlight.application.backoffice.mail.provided.BackofficeMailSendUseCase; +import starlight.application.backoffice.mail.provided.BackofficeMailTemplateUseCase; +import starlight.shared.apiPayload.response.ApiResponse; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class BackofficeMailController { + + private final BackofficeMailSendUseCase backofficeMailSendUseCase; + private final BackofficeMailTemplateUseCase templateUseCase; + + @PostMapping("/v1/backoffice/mail/send") + public ApiResponse send(@Valid @RequestBody BackofficeMailSendRequest request) { + return ApiResponse.success(BackofficeMailSendLogResponse.from( + backofficeMailSendUseCase.send(request.toInput()) + )); + } + + @PostMapping("/v1/backoffice/mail/templates") + public ApiResponse createTemplate( + @Valid @RequestBody BackofficeMailTemplateCreateRequest request + ) { + return ApiResponse.success(BackofficeMailTemplateResponse.from( + templateUseCase.createTemplate(request.toInput()) + )); + } + + @GetMapping("/v1/backoffice/mail/templates") + public ApiResponse> findTemplates() { + return ApiResponse.success(templateUseCase.findTemplates().stream() + .map(BackofficeMailTemplateResponse::from) + .toList()); + } + + @DeleteMapping("/v1/backoffice/mail/templates/{templateId}") + public ApiResponse deleteTemplate(@PathVariable Long templateId) { + templateUseCase.deleteTemplate(templateId); + return ApiResponse.success(null); + } + +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java new file mode 100644 index 00000000..a92a2f28 --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java @@ -0,0 +1,34 @@ +package starlight.adapter.backoffice.mail.webapi.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +import java.util.List; + +public record BackofficeMailSendRequest( + @NotEmpty(message = "to is required") + List<@Email @NotBlank String> to, + @NotBlank(message = "subject is required") + String subject, + @NotBlank(message = "contentType is required") + String contentType, + String html, + String text +) { + public BackofficeMailSendInput toInput() { + return new BackofficeMailSendInput( + to, + subject, + contentType, + html, + text + ); + } + + public BackofficeMailContentType toContentType() { + return BackofficeMailContentType.from(contentType); + } +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java new file mode 100644 index 00000000..23d4344a --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java @@ -0,0 +1,26 @@ +package starlight.adapter.backoffice.mail.webapi.dto.request; + +import jakarta.validation.constraints.NotBlank; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailTemplateCreateInput; +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +public record BackofficeMailTemplateCreateRequest( + @NotBlank(message = "name is required") + String name, + @NotBlank(message = "title is required") + String title, + @NotBlank(message = "contentType is required") + String contentType, + String html, + String text +) { + public BackofficeMailTemplateCreateInput toInput() { + return new BackofficeMailTemplateCreateInput( + name, + title, + BackofficeMailContentType.from(contentType), + html, + text + ); + } +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java new file mode 100644 index 00000000..26621f7a --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java @@ -0,0 +1,23 @@ +package starlight.adapter.backoffice.mail.webapi.dto.response; + +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; + +import java.time.LocalDateTime; + +public record BackofficeMailSendLogResponse( + String recipients, + String subject, + String contentType, + boolean success, + String errorMessage +) { + public static BackofficeMailSendLogResponse from(BackofficeMailSendLogResult result) { + return new BackofficeMailSendLogResponse( + result.recipients(), + result.subject(), + result.contentType(), + result.success(), + result.errorMessage() + ); + } +} diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailTemplateResponse.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailTemplateResponse.java new file mode 100644 index 00000000..c8d9533d --- /dev/null +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailTemplateResponse.java @@ -0,0 +1,27 @@ +package starlight.adapter.backoffice.mail.webapi.dto.response; + +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailTemplateResult; + +import java.time.LocalDateTime; + +public record BackofficeMailTemplateResponse( + Long id, + String name, + String title, + String contentType, + String html, + String text, + LocalDateTime createdAt +) { + public static BackofficeMailTemplateResponse from(BackofficeMailTemplateResult result) { + return new BackofficeMailTemplateResponse( + result.id(), + result.name(), + result.title(), + result.contentType(), + result.html(), + result.text(), + result.createdAt() + ); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java new file mode 100644 index 00000000..2565afc6 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java @@ -0,0 +1,55 @@ +package starlight.application.backoffice.mail; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import starlight.application.backoffice.mail.provided.BackofficeMailLogUseCase; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendLogCreateInput; +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; +import starlight.application.backoffice.mail.required.BackofficeMailSendLogCommandPort; +import starlight.domain.backoffice.exception.BackofficeErrorType; +import starlight.domain.backoffice.exception.BackofficeException; +import starlight.domain.backoffice.mail.BackofficeMailSendLog; + +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BackofficeMailLogService implements BackofficeMailLogUseCase { + + private final BackofficeMailSendLogCommandPort logCommandPort; + + @Override + @Transactional + public BackofficeMailSendLogResult createLog(BackofficeMailSendLogCreateInput input) { + String recipients = input.to().stream().collect(Collectors.joining(",")); + + BackofficeMailSendLog log = BackofficeMailSendLog.create( + recipients, + input.subject(), + input.contentType(), + input.success(), + input.errorMessage() + ); + + try { + BackofficeMailSendLog saved = logCommandPort.save(log); + return toResult(saved); + } catch (DataAccessException exception) { + throw new BackofficeException(BackofficeErrorType.MAIL_LOG_SAVE_FAILED); + } + } + + private BackofficeMailSendLogResult toResult(BackofficeMailSendLog log) { + return new BackofficeMailSendLogResult( + log.getId(), + log.getRecipients(), + log.getEmailTitle(), + log.getContentType().name().toLowerCase(), + log.isSuccess(), + log.getErrorMessage(), + log.getCreatedAt() + ); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java new file mode 100644 index 00000000..4a432deb --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java @@ -0,0 +1,87 @@ +package starlight.application.backoffice.mail; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import starlight.application.backoffice.mail.provided.BackofficeMailLogUseCase; +import starlight.application.backoffice.mail.provided.BackofficeMailSendUseCase; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendLogCreateInput; +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; +import starlight.application.backoffice.mail.required.MailSenderPort; +import starlight.domain.backoffice.exception.BackofficeErrorType; +import starlight.domain.backoffice.exception.BackofficeException; +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +@Service +@RequiredArgsConstructor +public class BackofficeMailSendService implements BackofficeMailSendUseCase { + + private final MailSenderPort mailSenderPort; + private final BackofficeMailLogUseCase logUseCase; + + @Override + @Transactional + public BackofficeMailSendLogResult send(BackofficeMailSendInput input) { + BackofficeMailContentType contentType = parseContentType(input.contentType()); + + try { + validate(input, contentType); + mailSenderPort.send(input, contentType); + return logUseCase.createLog(new BackofficeMailSendLogCreateInput( + input.to(), + input.subject(), + contentType, + true, + null + )); + } catch (IllegalArgumentException exception) { + return failAndThrow(input, contentType, exception.getMessage(), BackofficeErrorType.INVALID_MAIL_REQUEST); + } catch (Exception exception) { + return failAndThrow(input, contentType, exception.getMessage(), BackofficeErrorType.MAIL_SEND_FAILED); + } + } + + private BackofficeMailContentType parseContentType(String contentType) { + try { + return BackofficeMailContentType.from(contentType); + } catch (IllegalArgumentException exception) { + throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_CONTENT_TYPE); + } + } + + private void validate(BackofficeMailSendInput input, BackofficeMailContentType contentType) { + if (input.to() == null || input.to().isEmpty()) { + throw new IllegalArgumentException("recipient is required"); + } + if (input.subject() == null || input.subject().isBlank()) { + throw new IllegalArgumentException("subject is required"); + } + if (contentType == BackofficeMailContentType.HTML) { + if (input.html() == null || input.html().isBlank()) { + throw new IllegalArgumentException("html body is required"); + } + } + if (contentType == BackofficeMailContentType.TEXT) { + if (input.text() == null || input.text().isBlank()) { + throw new IllegalArgumentException("text body is required"); + } + } + } + + private BackofficeMailSendLogResult failAndThrow( + BackofficeMailSendInput input, + BackofficeMailContentType contentType, + String errorMessage, + BackofficeErrorType errorType + ) { + logUseCase.createLog(new BackofficeMailSendLogCreateInput( + input.to(), + input.subject(), + contentType, + false, + errorMessage + )); + throw new BackofficeException(errorType); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java new file mode 100644 index 00000000..0bdf4f4f --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java @@ -0,0 +1,77 @@ +package starlight.application.backoffice.mail; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import starlight.application.backoffice.mail.provided.BackofficeMailTemplateUseCase; +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailTemplateCreateInput; +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailTemplateResult; +import starlight.application.backoffice.mail.required.BackofficeMailTemplateCommandPort; +import starlight.application.backoffice.mail.required.BackofficeMailTemplateQueryPort; +import starlight.domain.backoffice.exception.BackofficeErrorType; +import starlight.domain.backoffice.exception.BackofficeException; +import starlight.domain.backoffice.mail.BackofficeMailTemplate; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BackofficeMailTemplateService implements BackofficeMailTemplateUseCase { + + private final BackofficeMailTemplateCommandPort templateCommandPort; + private final BackofficeMailTemplateQueryPort templateQueryPort; + + @Override + @Transactional + public BackofficeMailTemplateResult createTemplate(BackofficeMailTemplateCreateInput input) { + BackofficeMailTemplate template = BackofficeMailTemplate.create( + input.name(), + input.title(), + input.contentType(), + input.html(), + input.text() + ); + + try { + BackofficeMailTemplate saved = templateCommandPort.save(template); + return toResult(saved); + } catch (DataAccessException exception) { + throw new BackofficeException(BackofficeErrorType.MAIL_TEMPLATE_SAVE_FAILED); + } + } + + @Override + @Transactional(readOnly = true) + public List findTemplates() { + try { + return templateQueryPort.findAll().stream() + .map(this::toResult) + .toList(); + } catch (DataAccessException exception) { + throw new BackofficeException(BackofficeErrorType.MAIL_TEMPLATE_QUERY_FAILED); + } + } + + @Override + @Transactional + public void deleteTemplate(Long templateId) { + try { + templateCommandPort.deleteById(templateId); + } catch (DataAccessException exception) { + throw new BackofficeException(BackofficeErrorType.MAIL_TEMPLATE_DELETE_FAILED); + } + } + + private BackofficeMailTemplateResult toResult(BackofficeMailTemplate template) { + return new BackofficeMailTemplateResult( + template.getId(), + template.getName(), + template.getEmailTitle(), + template.getContentType().name().toLowerCase(), + template.getHtml(), + template.getText(), + template.getCreatedAt() + ); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java new file mode 100644 index 00000000..7cd84c76 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java @@ -0,0 +1,9 @@ +package starlight.application.backoffice.mail.provided; + +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendLogCreateInput; +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; + +public interface BackofficeMailLogUseCase { + + BackofficeMailSendLogResult createLog(BackofficeMailSendLogCreateInput input); +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java new file mode 100644 index 00000000..0b5cebcf --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java @@ -0,0 +1,9 @@ +package starlight.application.backoffice.mail.provided; + +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; + +public interface BackofficeMailSendUseCase { + + BackofficeMailSendLogResult send(BackofficeMailSendInput input); +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailTemplateUseCase.java b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailTemplateUseCase.java new file mode 100644 index 00000000..b79c0896 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailTemplateUseCase.java @@ -0,0 +1,15 @@ +package starlight.application.backoffice.mail.provided; + +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailTemplateCreateInput; +import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailTemplateResult; + +import java.util.List; + +public interface BackofficeMailTemplateUseCase { + + BackofficeMailTemplateResult createTemplate(BackofficeMailTemplateCreateInput input); + + List findTemplates(); + + void deleteTemplate(Long templateId); +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java new file mode 100644 index 00000000..4d0c59b6 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java @@ -0,0 +1,12 @@ +package starlight.application.backoffice.mail.provided.dto.input; + +import java.util.List; + +public record BackofficeMailSendInput( + List to, + String subject, + String contentType, + String html, + String text +) { +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java new file mode 100644 index 00000000..03271963 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java @@ -0,0 +1,14 @@ +package starlight.application.backoffice.mail.provided.dto.input; + +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +import java.util.List; + +public record BackofficeMailSendLogCreateInput( + List to, + String subject, + BackofficeMailContentType contentType, + boolean success, + String errorMessage +) { +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java new file mode 100644 index 00000000..90f82cef --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java @@ -0,0 +1,12 @@ +package starlight.application.backoffice.mail.provided.dto.input; + +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +public record BackofficeMailTemplateCreateInput( + String name, + String title, + BackofficeMailContentType contentType, + String html, + String text +) { +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java new file mode 100644 index 00000000..d9dce476 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java @@ -0,0 +1,14 @@ +package starlight.application.backoffice.mail.provided.dto.result; + +import java.time.LocalDateTime; + +public record BackofficeMailSendLogResult( + Long id, + String recipients, + String subject, + String contentType, + boolean success, + String errorMessage, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java new file mode 100644 index 00000000..183641c1 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java @@ -0,0 +1,14 @@ +package starlight.application.backoffice.mail.provided.dto.result; + +import java.time.LocalDateTime; + +public record BackofficeMailTemplateResult( + Long id, + String name, + String title, + String contentType, + String html, + String text, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailSendLogCommandPort.java b/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailSendLogCommandPort.java new file mode 100644 index 00000000..7d0815e0 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailSendLogCommandPort.java @@ -0,0 +1,8 @@ +package starlight.application.backoffice.mail.required; + +import starlight.domain.backoffice.mail.BackofficeMailSendLog; + +public interface BackofficeMailSendLogCommandPort { + + BackofficeMailSendLog save(BackofficeMailSendLog log); +} diff --git a/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateCommandPort.java b/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateCommandPort.java new file mode 100644 index 00000000..ff1bd221 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateCommandPort.java @@ -0,0 +1,10 @@ +package starlight.application.backoffice.mail.required; + +import starlight.domain.backoffice.mail.BackofficeMailTemplate; + +public interface BackofficeMailTemplateCommandPort { + + BackofficeMailTemplate save(BackofficeMailTemplate template); + + void deleteById(Long templateId); +} diff --git a/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateQueryPort.java b/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateQueryPort.java new file mode 100644 index 00000000..797933c4 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/required/BackofficeMailTemplateQueryPort.java @@ -0,0 +1,10 @@ +package starlight.application.backoffice.mail.required; + +import starlight.domain.backoffice.mail.BackofficeMailTemplate; + +import java.util.List; + +public interface BackofficeMailTemplateQueryPort { + + List findAll(); +} diff --git a/src/main/java/starlight/application/backoffice/mail/required/MailSenderPort.java b/src/main/java/starlight/application/backoffice/mail/required/MailSenderPort.java new file mode 100644 index 00000000..8da3a10a --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/required/MailSenderPort.java @@ -0,0 +1,9 @@ +package starlight.application.backoffice.mail.required; + +import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; +import starlight.domain.backoffice.mail.BackofficeMailContentType; + +public interface MailSenderPort { + + void send(BackofficeMailSendInput input, BackofficeMailContentType contentType); +} diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index dd30857e..ddce9d66 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -73,6 +73,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/", "/index.html", "/ops.html", "/payment.html", "/api/payment/**").permitAll() .requestMatchers("/v1/auth/**","/v1/user/**", "/v1/experts", "/v1/experts/*").permitAll() + .requestMatchers("/v1/backoffice/mail/**").permitAll() + .requestMatchers("/send-email").permitAll() .requestMatchers("/login/**", "/oauth2/**", "/login/oauth2/**", "/public/**").permitAll() .requestMatchers("/v3/api-docs/**", "/v1/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**").permitAll() diff --git a/src/main/java/starlight/domain/backoffice/exception/BackofficeErrorType.java b/src/main/java/starlight/domain/backoffice/exception/BackofficeErrorType.java new file mode 100644 index 00000000..a1d9f1f6 --- /dev/null +++ b/src/main/java/starlight/domain/backoffice/exception/BackofficeErrorType.java @@ -0,0 +1,24 @@ +package starlight.domain.backoffice.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import starlight.shared.apiPayload.exception.ErrorType; + +@Getter +@RequiredArgsConstructor +public enum BackofficeErrorType implements ErrorType { + + INVALID_MAIL_CONTENT_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 contentType입니다."), + INVALID_MAIL_REQUEST(HttpStatus.BAD_REQUEST, "메일 발송 요청이 유효하지 않습니다."), + MAIL_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "메일 전송에 실패했습니다."), + MAIL_TEMPLATE_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "메일 템플릿 저장에 실패했습니다."), + MAIL_TEMPLATE_QUERY_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "메일 템플릿 조회에 실패했습니다."), + MAIL_TEMPLATE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "메일 템플릿 삭제에 실패했습니다."), + MAIL_LOG_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "메일 로그 저장에 실패했습니다."), + ; + + private final HttpStatus status; + + private final String message; +} diff --git a/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java b/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java new file mode 100644 index 00000000..19bf61b7 --- /dev/null +++ b/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java @@ -0,0 +1,11 @@ +package starlight.domain.backoffice.exception; + +import starlight.shared.apiPayload.exception.ErrorType; +import starlight.shared.apiPayload.exception.GlobalException; + +public class BackofficeException extends GlobalException { + + public BackofficeException(ErrorType errorType) { + super(errorType); + } +} diff --git a/src/main/java/starlight/domain/backoffice/mail/BackofficeMailContentType.java b/src/main/java/starlight/domain/backoffice/mail/BackofficeMailContentType.java new file mode 100644 index 00000000..308c8bf7 --- /dev/null +++ b/src/main/java/starlight/domain/backoffice/mail/BackofficeMailContentType.java @@ -0,0 +1,26 @@ +package starlight.domain.backoffice.mail; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BackofficeMailContentType { + HTML("html"), + TEXT("텍스트"); + + private final String description; + + public static BackofficeMailContentType from(String value) { + if (value == null) { + throw new IllegalArgumentException("contentType is required"); + } + if ("html".equalsIgnoreCase(value)) { + return HTML; + } + if ("text".equalsIgnoreCase(value)) { + return TEXT; + } + throw new IllegalArgumentException("invalid contentType"); + } +} \ No newline at end of file diff --git a/src/main/java/starlight/domain/backoffice/mail/BackofficeMailSendLog.java b/src/main/java/starlight/domain/backoffice/mail/BackofficeMailSendLog.java new file mode 100644 index 00000000..dc29114e --- /dev/null +++ b/src/main/java/starlight/domain/backoffice/mail/BackofficeMailSendLog.java @@ -0,0 +1,50 @@ +package starlight.domain.backoffice.mail; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.Assert; +import starlight.shared.AbstractEntity; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BackofficeMailSendLog extends AbstractEntity { + + @Column(nullable = false, columnDefinition = "TEXT") + private String recipients; + + @Column(nullable = false) + private String emailTitle; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private BackofficeMailContentType contentType; + + @Column(nullable = false) + private boolean success; + + @Column(columnDefinition = "TEXT") + private String errorMessage; + + public static BackofficeMailSendLog create( + String recipients, String emailTitle, BackofficeMailContentType contentType, boolean success, String errorMessage + ) { + Assert.hasText(recipients, "recipients must not be empty"); + Assert.hasText(emailTitle, "subject must not be empty"); + Assert.notNull(contentType, "contentType must not be null"); + + BackofficeMailSendLog log = new BackofficeMailSendLog(); + log.recipients = recipients; + log.emailTitle = emailTitle; + log.contentType = contentType; + log.success = success; + log.errorMessage = errorMessage; + + return log; + } +} diff --git a/src/main/java/starlight/domain/backoffice/mail/BackofficeMailTemplate.java b/src/main/java/starlight/domain/backoffice/mail/BackofficeMailTemplate.java new file mode 100644 index 00000000..feee431f --- /dev/null +++ b/src/main/java/starlight/domain/backoffice/mail/BackofficeMailTemplate.java @@ -0,0 +1,50 @@ +package starlight.domain.backoffice.mail; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.Assert; +import starlight.shared.AbstractEntity; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BackofficeMailTemplate extends AbstractEntity { + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String emailTitle; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private BackofficeMailContentType contentType; + + @Column(columnDefinition = "TEXT") + private String html; + + @Column(columnDefinition = "TEXT") + private String text; + + public static BackofficeMailTemplate create( + String name, String emailTitle, BackofficeMailContentType contentType, String html, String text + ) { + Assert.hasText(name, "name must not be empty"); + Assert.hasText(emailTitle, "title must not be empty"); + Assert.notNull(contentType, "contentType must not be null"); + + BackofficeMailTemplate template = new BackofficeMailTemplate(); + template.name = name; + template.emailTitle = emailTitle; + template.contentType = contentType; + template.html = html; + template.text = text; + + return template; + } +} From 81409ec169e007145b2417983ff5c94b8977c674 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Fri, 16 Jan 2026 01:55:12 +0900 Subject: [PATCH 02/17] =?UTF-8?q?[SRLT-132]=20feat:=20=EB=B0=B1=EC=98=A4?= =?UTF-8?q?=ED=94=BC=EC=8A=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- .../starlight/bootstrap/SecurityConfig.java | 59 ++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/config b/config index 5acba1ec..bcc30ec0 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 5acba1ecc85733f96efaa9c60db314292dca268f +Subproject commit bcc30ec0d11eb5663b114a3b6c73894d67abbc01 diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index ddce9d66..e758f299 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -15,6 +16,15 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; @@ -40,6 +50,8 @@ public class SecurityConfig { @Value("${cors.origin.server}") String ServerBaseUrl; @Value("${cors.origin.client}") String clientBaseUrl; @Value("${cors.origin.develop}") String devBaseUrl; + @Value("${backoffice.auth.username}") String backofficeUsername; + @Value("${backoffice.auth.password}") String backofficePassword; private final JwtFilter jwtFilter; private final ExceptionFilter exceptionFilter; @@ -49,6 +61,30 @@ public class SecurityConfig { private final OAuth2SuccessHandler oAuth2SuccessHandler; @Bean + @Order(1) + public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Exception { + CsrfTokenRequestAttributeHandler csrfTokenRequestHandler = new CsrfTokenRequestAttributeHandler(); + + http.securityMatcher("/v1/backoffice/mail/**", "/login", "/logout") + .cors(Customizer.withDefaults()) + .csrf((csrf) -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(csrfTokenRequestHandler) + ) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .authorizeHttpRequests((authorize) -> + authorize + .requestMatchers("/login", "/logout").permitAll() + .anyRequest().hasRole("BACKOFFICE") + ) + .formLogin(Customizer.withDefaults()) + .logout(Customizer.withDefaults()); + + return http.build(); + } + + @Bean + @Order(2) public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable); @@ -73,8 +109,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/", "/index.html", "/ops.html", "/payment.html", "/api/payment/**").permitAll() .requestMatchers("/v1/auth/**","/v1/user/**", "/v1/experts", "/v1/experts/*").permitAll() - .requestMatchers("/v1/backoffice/mail/**").permitAll() - .requestMatchers("/send-email").permitAll() .requestMatchers("/login/**", "/oauth2/**", "/login/oauth2/**", "/public/**").permitAll() .requestMatchers("/v3/api-docs/**", "/v1/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**").permitAll() @@ -124,6 +158,27 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean + public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails user = User.builder() + .username(backofficeUsername) + .password(passwordEncoder.encode(backofficePassword)) + .roles("BACKOFFICE") + .build(); + return new InMemoryUserDetailsManager(user); + } + + @Bean + public AuthenticationProvider authenticationProvider( + UserDetailsService userDetailsService, + PasswordEncoder passwordEncoder + ) { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder); + return provider; + } + @Bean public LogoutSuccessHandler logoutSuccessHandler() { return new HttpStatusReturningLogoutSuccessHandler(); From ef24a8cbd8282f270ffb045162fabbc4c834700a Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Fri, 16 Jan 2026 17:01:52 +0900 Subject: [PATCH 03/17] =?UTF-8?q?[SRLT-132]=20feat:=20event=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=EB=A1=9C=EA=B7=B8=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=88=98=EC=88=9C=EC=9D=84=20=EB=A9=94=EC=9D=B8=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=EC=84=9C=20=EB=B6=84=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- .../mail/webapi/BackofficeMailController.java | 22 ++++---- .../request/BackofficeMailSendRequest.java | 13 +---- .../BackofficeMailTemplateCreateRequest.java | 8 +-- .../BackofficeMailSendLogResponse.java | 23 -------- .../mail/BackofficeMailLogService.java | 55 ------------------- .../BackofficeMailSendLogEventHandler.java | 28 ++++++++++ .../mail/BackofficeMailSendService.java | 26 ++++----- .../BackofficeMailSendEvent.java} | 4 +- .../provided/BackofficeMailLogUseCase.java | 9 --- .../provided/BackofficeMailSendUseCase.java | 3 +- .../dto/input/BackofficeMailSendInput.java | 3 +- .../BackofficeMailTemplateCreateInput.java | 3 +- .../result/BackofficeMailSendLogResult.java | 14 ----- 14 files changed, 60 insertions(+), 153 deletions(-) delete mode 100644 src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java delete mode 100644 src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java create mode 100644 src/main/java/starlight/application/backoffice/mail/BackofficeMailSendLogEventHandler.java rename src/main/java/starlight/application/backoffice/mail/{provided/dto/input/BackofficeMailSendLogCreateInput.java => event/BackofficeMailSendEvent.java} (68%) delete mode 100644 src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java delete mode 100644 src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java diff --git a/config b/config index bcc30ec0..711559bd 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit bcc30ec0d11eb5663b114a3b6c73894d67abbc01 +Subproject commit 711559bd8b08983d93e3fb61ffbd3f0b475f639d diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java index a4d24f89..e4c267d0 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java @@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import starlight.adapter.backoffice.mail.webapi.dto.request.BackofficeMailSendRequest; -import starlight.adapter.backoffice.mail.webapi.dto.response.BackofficeMailSendLogResponse; import starlight.adapter.backoffice.mail.webapi.dto.request.BackofficeMailTemplateCreateRequest; import starlight.adapter.backoffice.mail.webapi.dto.response.BackofficeMailTemplateResponse; import starlight.application.backoffice.mail.provided.BackofficeMailSendUseCase; @@ -21,19 +20,19 @@ public class BackofficeMailController { private final BackofficeMailTemplateUseCase templateUseCase; @PostMapping("/v1/backoffice/mail/send") - public ApiResponse send(@Valid @RequestBody BackofficeMailSendRequest request) { - return ApiResponse.success(BackofficeMailSendLogResponse.from( - backofficeMailSendUseCase.send(request.toInput()) - )); + public ApiResponse send( + @Valid @RequestBody BackofficeMailSendRequest request + ) { + backofficeMailSendUseCase.send(request.toInput()); + return ApiResponse.success("이메일 전송 성공"); } @PostMapping("/v1/backoffice/mail/templates") public ApiResponse createTemplate( @Valid @RequestBody BackofficeMailTemplateCreateRequest request ) { - return ApiResponse.success(BackofficeMailTemplateResponse.from( - templateUseCase.createTemplate(request.toInput()) - )); + BackofficeMailTemplateResponse response = BackofficeMailTemplateResponse.from(templateUseCase.createTemplate(request.toInput())); + return ApiResponse.success(response); } @GetMapping("/v1/backoffice/mail/templates") @@ -44,9 +43,10 @@ public ApiResponse> findTemplates() { } @DeleteMapping("/v1/backoffice/mail/templates/{templateId}") - public ApiResponse deleteTemplate(@PathVariable Long templateId) { + public ApiResponse deleteTemplate( + @PathVariable Long templateId + ) { templateUseCase.deleteTemplate(templateId); - return ApiResponse.success(null); + return ApiResponse.success("템플릿이 삭제되었습니다."); } - } diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java index a92a2f28..514ddcc8 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; -import starlight.domain.backoffice.mail.BackofficeMailContentType; import java.util.List; @@ -19,16 +18,6 @@ public record BackofficeMailSendRequest( String text ) { public BackofficeMailSendInput toInput() { - return new BackofficeMailSendInput( - to, - subject, - contentType, - html, - text - ); - } - - public BackofficeMailContentType toContentType() { - return BackofficeMailContentType.from(contentType); + return new BackofficeMailSendInput(to, subject, contentType, html, text); } } diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java index 23d4344a..a185647f 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java @@ -15,12 +15,6 @@ public record BackofficeMailTemplateCreateRequest( String text ) { public BackofficeMailTemplateCreateInput toInput() { - return new BackofficeMailTemplateCreateInput( - name, - title, - BackofficeMailContentType.from(contentType), - html, - text - ); + return new BackofficeMailTemplateCreateInput(name, title, BackofficeMailContentType.from(contentType), html, text); } } diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java deleted file mode 100644 index 26621f7a..00000000 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/response/BackofficeMailSendLogResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package starlight.adapter.backoffice.mail.webapi.dto.response; - -import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; - -import java.time.LocalDateTime; - -public record BackofficeMailSendLogResponse( - String recipients, - String subject, - String contentType, - boolean success, - String errorMessage -) { - public static BackofficeMailSendLogResponse from(BackofficeMailSendLogResult result) { - return new BackofficeMailSendLogResponse( - result.recipients(), - result.subject(), - result.contentType(), - result.success(), - result.errorMessage() - ); - } -} diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java deleted file mode 100644 index 2565afc6..00000000 --- a/src/main/java/starlight/application/backoffice/mail/BackofficeMailLogService.java +++ /dev/null @@ -1,55 +0,0 @@ -package starlight.application.backoffice.mail; - -import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataAccessException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import starlight.application.backoffice.mail.provided.BackofficeMailLogUseCase; -import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendLogCreateInput; -import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; -import starlight.application.backoffice.mail.required.BackofficeMailSendLogCommandPort; -import starlight.domain.backoffice.exception.BackofficeErrorType; -import starlight.domain.backoffice.exception.BackofficeException; -import starlight.domain.backoffice.mail.BackofficeMailSendLog; - -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class BackofficeMailLogService implements BackofficeMailLogUseCase { - - private final BackofficeMailSendLogCommandPort logCommandPort; - - @Override - @Transactional - public BackofficeMailSendLogResult createLog(BackofficeMailSendLogCreateInput input) { - String recipients = input.to().stream().collect(Collectors.joining(",")); - - BackofficeMailSendLog log = BackofficeMailSendLog.create( - recipients, - input.subject(), - input.contentType(), - input.success(), - input.errorMessage() - ); - - try { - BackofficeMailSendLog saved = logCommandPort.save(log); - return toResult(saved); - } catch (DataAccessException exception) { - throw new BackofficeException(BackofficeErrorType.MAIL_LOG_SAVE_FAILED); - } - } - - private BackofficeMailSendLogResult toResult(BackofficeMailSendLog log) { - return new BackofficeMailSendLogResult( - log.getId(), - log.getRecipients(), - log.getEmailTitle(), - log.getContentType().name().toLowerCase(), - log.isSuccess(), - log.getErrorMessage(), - log.getCreatedAt() - ); - } -} diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendLogEventHandler.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendLogEventHandler.java new file mode 100644 index 00000000..41928262 --- /dev/null +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendLogEventHandler.java @@ -0,0 +1,28 @@ +package starlight.application.backoffice.mail; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import starlight.application.backoffice.mail.event.BackofficeMailSendEvent; +import starlight.application.backoffice.mail.required.BackofficeMailSendLogCommandPort; +import starlight.domain.backoffice.mail.BackofficeMailSendLog; + +@Component +@RequiredArgsConstructor +public class BackofficeMailSendLogEventHandler { + + private final BackofficeMailSendLogCommandPort logCommandPort; + + @EventListener + public void handle(BackofficeMailSendEvent event) { + String recipients = String.join(",", event.to()); + BackofficeMailSendLog log = BackofficeMailSendLog.create( + recipients, + event.subject(), + event.contentType(), + event.success(), + event.errorMessage() + ); + logCommandPort.save(log); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java index 4a432deb..565a10a4 100644 --- a/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java @@ -3,12 +3,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import starlight.application.backoffice.mail.provided.BackofficeMailLogUseCase; import starlight.application.backoffice.mail.provided.BackofficeMailSendUseCase; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; -import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendLogCreateInput; -import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; import starlight.application.backoffice.mail.required.MailSenderPort; +import starlight.application.backoffice.mail.event.BackofficeMailSendEvent; +import org.springframework.context.ApplicationEventPublisher; import starlight.domain.backoffice.exception.BackofficeErrorType; import starlight.domain.backoffice.exception.BackofficeException; import starlight.domain.backoffice.mail.BackofficeMailContentType; @@ -18,27 +17,30 @@ public class BackofficeMailSendService implements BackofficeMailSendUseCase { private final MailSenderPort mailSenderPort; - private final BackofficeMailLogUseCase logUseCase; + private final ApplicationEventPublisher eventPublisher; @Override @Transactional - public BackofficeMailSendLogResult send(BackofficeMailSendInput input) { + public void send(BackofficeMailSendInput input) { BackofficeMailContentType contentType = parseContentType(input.contentType()); try { validate(input, contentType); mailSenderPort.send(input, contentType); - return logUseCase.createLog(new BackofficeMailSendLogCreateInput( + eventPublisher.publishEvent(new BackofficeMailSendEvent( input.to(), input.subject(), contentType, true, null )); + return; } catch (IllegalArgumentException exception) { - return failAndThrow(input, contentType, exception.getMessage(), BackofficeErrorType.INVALID_MAIL_REQUEST); + publishFailureEvent(input, contentType, exception.getMessage()); + throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_REQUEST); } catch (Exception exception) { - return failAndThrow(input, contentType, exception.getMessage(), BackofficeErrorType.MAIL_SEND_FAILED); + publishFailureEvent(input, contentType, exception.getMessage()); + throw new BackofficeException(BackofficeErrorType.MAIL_SEND_FAILED); } } @@ -69,19 +71,17 @@ private void validate(BackofficeMailSendInput input, BackofficeMailContentType c } } - private BackofficeMailSendLogResult failAndThrow( + private void publishFailureEvent( BackofficeMailSendInput input, BackofficeMailContentType contentType, - String errorMessage, - BackofficeErrorType errorType + String errorMessage ) { - logUseCase.createLog(new BackofficeMailSendLogCreateInput( + eventPublisher.publishEvent(new BackofficeMailSendEvent( input.to(), input.subject(), contentType, false, errorMessage )); - throw new BackofficeException(errorType); } } diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java b/src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java similarity index 68% rename from src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java rename to src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java index 03271963..1f47d370 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendLogCreateInput.java +++ b/src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java @@ -1,10 +1,10 @@ -package starlight.application.backoffice.mail.provided.dto.input; +package starlight.application.backoffice.mail.event; import starlight.domain.backoffice.mail.BackofficeMailContentType; import java.util.List; -public record BackofficeMailSendLogCreateInput( +public record BackofficeMailSendEvent( List to, String subject, BackofficeMailContentType contentType, diff --git a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java deleted file mode 100644 index 7cd84c76..00000000 --- a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailLogUseCase.java +++ /dev/null @@ -1,9 +0,0 @@ -package starlight.application.backoffice.mail.provided; - -import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendLogCreateInput; -import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; - -public interface BackofficeMailLogUseCase { - - BackofficeMailSendLogResult createLog(BackofficeMailSendLogCreateInput input); -} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java index 0b5cebcf..37824882 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java +++ b/src/main/java/starlight/application/backoffice/mail/provided/BackofficeMailSendUseCase.java @@ -1,9 +1,8 @@ package starlight.application.backoffice.mail.provided; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; -import starlight.application.backoffice.mail.provided.dto.result.BackofficeMailSendLogResult; public interface BackofficeMailSendUseCase { - BackofficeMailSendLogResult send(BackofficeMailSendInput input); + void send(BackofficeMailSendInput input); } diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java index 4d0c59b6..2a4aae96 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java @@ -8,5 +8,4 @@ public record BackofficeMailSendInput( String contentType, String html, String text -) { -} +) { } diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java index 90f82cef..c55b83da 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java @@ -8,5 +8,4 @@ public record BackofficeMailTemplateCreateInput( BackofficeMailContentType contentType, String html, String text -) { -} +) { } diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java deleted file mode 100644 index d9dce476..00000000 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailSendLogResult.java +++ /dev/null @@ -1,14 +0,0 @@ -package starlight.application.backoffice.mail.provided.dto.result; - -import java.time.LocalDateTime; - -public record BackofficeMailSendLogResult( - Long id, - String recipients, - String subject, - String contentType, - boolean success, - String errorMessage, - LocalDateTime createdAt -) { -} From e5b3956767961c67a2ea485de703e7551e1ee4dc Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Fri, 16 Jan 2026 17:26:51 +0900 Subject: [PATCH 04/17] =?UTF-8?q?[SRLT-132]=20Refactor:=20=EB=B0=B1?= =?UTF-8?q?=EC=98=A4=ED=94=BC=EC=8A=A4=20=EB=A9=94=EC=9D=BC=20DTO=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=EC=A0=81=20=ED=8C=A9=ED=86=A0=EB=A6=AC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mail/webapi/BackofficeMailController.java | 2 +- .../request/BackofficeMailSendRequest.java | 2 +- .../BackofficeMailTemplateCreateRequest.java | 4 +--- .../mail/BackofficeMailSendService.java | 10 ++++++---- .../mail/BackofficeMailTemplateService.java | 14 ++++++++++++-- .../mail/event/BackofficeMailSendEvent.java | 9 +++++++++ .../dto/input/BackofficeMailSendInput.java | 12 +++++++++++- .../BackofficeMailTemplateCreateInput.java | 16 ++++++++++++---- .../result/BackofficeMailTemplateResult.java | 19 +++++++++++++++++++ 9 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java index e4c267d0..f6492534 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/BackofficeMailController.java @@ -24,7 +24,7 @@ public ApiResponse send( @Valid @RequestBody BackofficeMailSendRequest request ) { backofficeMailSendUseCase.send(request.toInput()); - return ApiResponse.success("이메일 전송 성공"); + return ApiResponse.success("이메일 전송에 성공하였습니다."); } @PostMapping("/v1/backoffice/mail/templates") diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java index 514ddcc8..24b17631 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java @@ -18,6 +18,6 @@ public record BackofficeMailSendRequest( String text ) { public BackofficeMailSendInput toInput() { - return new BackofficeMailSendInput(to, subject, contentType, html, text); + return BackofficeMailSendInput.of(to, subject, contentType, html, text); } } diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java index a185647f..81974a64 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailTemplateCreateRequest.java @@ -2,8 +2,6 @@ import jakarta.validation.constraints.NotBlank; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailTemplateCreateInput; -import starlight.domain.backoffice.mail.BackofficeMailContentType; - public record BackofficeMailTemplateCreateRequest( @NotBlank(message = "name is required") String name, @@ -15,6 +13,6 @@ public record BackofficeMailTemplateCreateRequest( String text ) { public BackofficeMailTemplateCreateInput toInput() { - return new BackofficeMailTemplateCreateInput(name, title, BackofficeMailContentType.from(contentType), html, text); + return BackofficeMailTemplateCreateInput.of(name, title, contentType, html, text); } } diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java index 565a10a4..07243780 100644 --- a/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailSendService.java @@ -11,6 +11,7 @@ import starlight.domain.backoffice.exception.BackofficeErrorType; import starlight.domain.backoffice.exception.BackofficeException; import starlight.domain.backoffice.mail.BackofficeMailContentType; +import starlight.domain.backoffice.mail.BackofficeMailSendLog; @Service @RequiredArgsConstructor @@ -27,14 +28,15 @@ public void send(BackofficeMailSendInput input) { try { validate(input, contentType); mailSenderPort.send(input, contentType); - eventPublisher.publishEvent(new BackofficeMailSendEvent( + BackofficeMailSendEvent log = BackofficeMailSendEvent.of( input.to(), input.subject(), contentType, true, null - )); - return; + ); + eventPublisher.publishEvent(log); + } catch (IllegalArgumentException exception) { publishFailureEvent(input, contentType, exception.getMessage()); throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_REQUEST); @@ -76,7 +78,7 @@ private void publishFailureEvent( BackofficeMailContentType contentType, String errorMessage ) { - eventPublisher.publishEvent(new BackofficeMailSendEvent( + eventPublisher.publishEvent(BackofficeMailSendEvent.of( input.to(), input.subject(), contentType, diff --git a/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java b/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java index 0bdf4f4f..bdd12e9f 100644 --- a/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java +++ b/src/main/java/starlight/application/backoffice/mail/BackofficeMailTemplateService.java @@ -11,6 +11,7 @@ import starlight.application.backoffice.mail.required.BackofficeMailTemplateQueryPort; import starlight.domain.backoffice.exception.BackofficeErrorType; import starlight.domain.backoffice.exception.BackofficeException; +import starlight.domain.backoffice.mail.BackofficeMailContentType; import starlight.domain.backoffice.mail.BackofficeMailTemplate; import java.util.List; @@ -25,10 +26,11 @@ public class BackofficeMailTemplateService implements BackofficeMailTemplateUseC @Override @Transactional public BackofficeMailTemplateResult createTemplate(BackofficeMailTemplateCreateInput input) { + BackofficeMailContentType contentType = parseContentType(input.contentType()); BackofficeMailTemplate template = BackofficeMailTemplate.create( input.name(), input.title(), - input.contentType(), + contentType, input.html(), input.text() ); @@ -41,6 +43,14 @@ public BackofficeMailTemplateResult createTemplate(BackofficeMailTemplateCreateI } } + private BackofficeMailContentType parseContentType(String contentType) { + try { + return BackofficeMailContentType.from(contentType); + } catch (IllegalArgumentException exception) { + throw new BackofficeException(BackofficeErrorType.INVALID_MAIL_CONTENT_TYPE); + } + } + @Override @Transactional(readOnly = true) public List findTemplates() { @@ -64,7 +74,7 @@ public void deleteTemplate(Long templateId) { } private BackofficeMailTemplateResult toResult(BackofficeMailTemplate template) { - return new BackofficeMailTemplateResult( + return BackofficeMailTemplateResult.of( template.getId(), template.getName(), template.getEmailTitle(), diff --git a/src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java b/src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java index 1f47d370..b13163b1 100644 --- a/src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java +++ b/src/main/java/starlight/application/backoffice/mail/event/BackofficeMailSendEvent.java @@ -11,4 +11,13 @@ public record BackofficeMailSendEvent( boolean success, String errorMessage ) { + public static BackofficeMailSendEvent of( + List to, + String subject, + BackofficeMailContentType contentType, + boolean success, + String errorMessage + ) { + return new BackofficeMailSendEvent(to, subject, contentType, success, errorMessage); + } } diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java index 2a4aae96..dde0c20c 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailSendInput.java @@ -8,4 +8,14 @@ public record BackofficeMailSendInput( String contentType, String html, String text -) { } +) { + public static BackofficeMailSendInput of( + List to, + String subject, + String contentType, + String html, + String text + ) { + return new BackofficeMailSendInput(to, subject, contentType, html, text); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java index c55b83da..83867cf0 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/input/BackofficeMailTemplateCreateInput.java @@ -1,11 +1,19 @@ package starlight.application.backoffice.mail.provided.dto.input; -import starlight.domain.backoffice.mail.BackofficeMailContentType; - public record BackofficeMailTemplateCreateInput( String name, String title, - BackofficeMailContentType contentType, + String contentType, String html, String text -) { } +) { + public static BackofficeMailTemplateCreateInput of( + String name, + String title, + String contentType, + String html, + String text + ) { + return new BackofficeMailTemplateCreateInput(name, title, contentType, html, text); + } +} diff --git a/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java index 183641c1..bbe225b6 100644 --- a/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java +++ b/src/main/java/starlight/application/backoffice/mail/provided/dto/result/BackofficeMailTemplateResult.java @@ -11,4 +11,23 @@ public record BackofficeMailTemplateResult( String text, LocalDateTime createdAt ) { + public static BackofficeMailTemplateResult of( + Long id, + String name, + String title, + String contentType, + String html, + String text, + LocalDateTime createdAt + ) { + return new BackofficeMailTemplateResult( + id, + name, + title, + contentType, + html, + text, + createdAt + ); + } } From 39f4e87a8e439b5b3b8835d993756b40f8305572 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Fri, 16 Jan 2026 17:30:06 +0900 Subject: [PATCH 05/17] =?UTF-8?q?[SRLT-132]=20Chore:=20=EB=B0=B1=EC=98=A4?= =?UTF-8?q?=ED=94=BC=EC=8A=A4=20=EC=A3=BC=EC=86=8C=EB=A5=BC=20cors=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=97=90=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- src/main/java/starlight/bootstrap/SecurityConfig.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config b/config index 711559bd..5a95ffdd 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 711559bd8b08983d93e3fb61ffbd3f0b475f639d +Subproject commit 5a95ffdd23c40f8409f39c70d256d5705d8ae856 diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index e758f299..734f6eea 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -49,6 +49,7 @@ public class SecurityConfig { @Value("${cors.origin.server}") String ServerBaseUrl; @Value("${cors.origin.client}") String clientBaseUrl; + @Value("${cors.origin.office}") String officeBaseUrl; @Value("${cors.origin.develop}") String devBaseUrl; @Value("${backoffice.auth.username}") String backofficeUsername; @Value("${backoffice.auth.password}") String backofficePassword; @@ -138,7 +139,8 @@ public CorsConfigurationSource corsConfigurationSource() { configuration.setAllowedOrigins(List.of( clientBaseUrl, ServerBaseUrl, - devBaseUrl + devBaseUrl, + officeBaseUrl )); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); From cb3023ed3607fff2b45e1fd5c2e0b2ef6892266a Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Fri, 16 Jan 2026 17:34:22 +0900 Subject: [PATCH 06/17] =?UTF-8?q?[SRLT-132]=20Chore:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index 5a95ffdd..17476065 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 5a95ffdd23c40f8409f39c70d256d5705d8ae856 +Subproject commit 17476065de9fec54bf5604f1c9e96932bda3c34c From 417fe2f1c6bb30d47c5d31b6ccbe5631b88745e3 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 01:12:36 +0900 Subject: [PATCH 07/17] =?UTF-8?q?[SRLT-132]=20Fix:=20CSRF=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20SameSite/Secure=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/starlight/bootstrap/SecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index 734f6eea..11633bb3 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -65,11 +65,13 @@ public class SecurityConfig { @Order(1) public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Exception { CsrfTokenRequestAttributeHandler csrfTokenRequestHandler = new CsrfTokenRequestAttributeHandler(); + CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + csrfTokenRepository.setCookieCustomizer(cookie -> cookie.sameSite("None").secure(true)); http.securityMatcher("/v1/backoffice/mail/**", "/login", "/logout") .cors(Customizer.withDefaults()) .csrf((csrf) -> csrf - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRepository(csrfTokenRepository) .csrfTokenRequestHandler(csrfTokenRequestHandler) ) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) From 1c82af182090681cc5d9ea13b77eb8556e4696b3 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 12:00:32 +0900 Subject: [PATCH 08/17] =?UTF-8?q?[SRLT-132]=20Fix:=20CSRF=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20=EB=8F=84=EB=A9=94=EC=9D=B8/SameSite/Secure=20?= =?UTF-8?q?=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/starlight/bootstrap/SecurityConfig.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index 11633bb3..65f5e776 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -66,7 +66,11 @@ public class SecurityConfig { public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Exception { CsrfTokenRequestAttributeHandler csrfTokenRequestHandler = new CsrfTokenRequestAttributeHandler(); CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); - csrfTokenRepository.setCookieCustomizer(cookie -> cookie.sameSite("None").secure(true)); + csrfTokenRepository.setCookieCustomizer(cookie -> cookie + .domain(".starlight-official.co.kr") + .sameSite("None") + .secure(true) + ); http.securityMatcher("/v1/backoffice/mail/**", "/login", "/logout") .cors(Customizer.withDefaults()) From 1d5973dce4aa75d08a3cafc4fddf0ac14ead0541 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 13:47:49 +0900 Subject: [PATCH 09/17] =?UTF-8?q?[SRLT-132]=20Fix:=20.=20=EB=B9=BC?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/starlight/bootstrap/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index 65f5e776..e5c004f4 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -67,7 +67,7 @@ public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Excep CsrfTokenRequestAttributeHandler csrfTokenRequestHandler = new CsrfTokenRequestAttributeHandler(); CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); csrfTokenRepository.setCookieCustomizer(cookie -> cookie - .domain(".starlight-official.co.kr") + .domain("starlight-official.co.kr") .sameSite("None") .secure(true) ); From 824890f839d35aa44dafd1d8f9bc093db5a57563 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 14:26:05 +0900 Subject: [PATCH 10/17] =?UTF-8?q?[SRLT-132]=20Fix:=20CSRF=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20=EB=8F=84=EB=A9=94=EC=9D=B8/SameSite/Secure=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index 17476065..3d7570eb 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 17476065de9fec54bf5604f1c9e96932bda3c34c +Subproject commit 3d7570eb05dcac08e5663f31307ec34551d32bff From 9301af145fec4d26d70c9380a1625a79d248463f Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 14:50:01 +0900 Subject: [PATCH 11/17] =?UTF-8?q?[SRLT-132]=20Fix:=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20remoteip=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index 3d7570eb..e3521f90 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 3d7570eb05dcac08e5663f31307ec34551d32bff +Subproject commit e3521f9022064bbcddf6c10d27ade6fc40651709 From fe0e2c39589631ddf0e468492a3d2901c1cf3ce7 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 15:05:34 +0900 Subject: [PATCH 12/17] =?UTF-8?q?[SRLT-132]=20Fix:=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20remoteip=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index e3521f90..a9d98d9b 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit e3521f9022064bbcddf6c10d27ade6fc40651709 +Subproject commit a9d98d9b2ad05b4d753fd0abeb264508317418c3 From 89ac496c9cb37aced78ecddb9cf0d96d001d18cc Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 15:33:17 +0900 Subject: [PATCH 13/17] =?UTF-8?q?[SRLT-132]=20Fix:=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/starlight/bootstrap/SecurityConfig.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index e5c004f4..97ed9a4a 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -54,6 +55,7 @@ public class SecurityConfig { @Value("${backoffice.auth.username}") String backofficeUsername; @Value("${backoffice.auth.password}") String backofficePassword; + private final Environment environment; private final JwtFilter jwtFilter; private final ExceptionFilter exceptionFilter; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; @@ -66,17 +68,21 @@ public class SecurityConfig { public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Exception { CsrfTokenRequestAttributeHandler csrfTokenRequestHandler = new CsrfTokenRequestAttributeHandler(); CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); - csrfTokenRepository.setCookieCustomizer(cookie -> cookie - .domain("starlight-official.co.kr") - .sameSite("None") - .secure(true) - ); + boolean isDevProfile = List.of(environment.getActiveProfiles()).contains("dev"); + if (!isDevProfile) { + csrfTokenRepository.setCookieCustomizer(cookie -> cookie + .domain("starlight-official.co.kr") + .sameSite("None") + .secure(true) + ); + } http.securityMatcher("/v1/backoffice/mail/**", "/login", "/logout") .cors(Customizer.withDefaults()) .csrf((csrf) -> csrf .csrfTokenRepository(csrfTokenRepository) .csrfTokenRequestHandler(csrfTokenRequestHandler) + .ignoringRequestMatchers("/login", "/logout") ) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .authorizeHttpRequests((authorize) -> From f45cb620cd410c830fd2effe3320f08108b8bb86 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 15:52:50 +0900 Subject: [PATCH 14/17] =?UTF-8?q?[SRLT-132]=20Fix:=20=EB=B0=B1=EC=98=A4?= =?UTF-8?q?=ED=94=BC=EC=8A=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20HTTPS=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EC=8B=9C=20X-Forwarded-*=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20HTTPS=20redirect=20-=20mixed-content=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EB=AC=B8=EC=A0=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starlight/bootstrap/SecurityConfig.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index 97ed9a4a..e47c53d3 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -90,7 +90,22 @@ public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Excep .requestMatchers("/login", "/logout").permitAll() .anyRequest().hasRole("BACKOFFICE") ) - .formLogin(Customizer.withDefaults()) + .formLogin((form) -> form.successHandler((request, response, authentication) -> { + String proto = request.getHeader("X-Forwarded-Proto"); + if (proto == null || proto.isBlank()) { + proto = request.getScheme(); + } + String host = request.getHeader("X-Forwarded-Host"); + if (host == null || host.isBlank()) { + host = request.getServerName(); + } + String port = request.getHeader("X-Forwarded-Port"); + String portPart = ""; + if (port != null && !port.isBlank() && !"443".equals(port) && !"80".equals(port)) { + portPart = ":" + port; + } + response.sendRedirect(proto + "://" + host + portPart + "/"); + })) .logout(Customizer.withDefaults()); return http.build(); From 73f59b0b3c33cb3d165a5f52bdfb2b6f3d0940ff Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Sat, 17 Jan 2026 16:23:38 +0900 Subject: [PATCH 15/17] =?UTF-8?q?[SRLT-132]=20Fix:=20=EB=B0=B1=EC=98=A4?= =?UTF-8?q?=ED=94=BC=EC=8A=A4=EC=AA=BD=EC=9C=BC=EB=A1=9C=20Redirect=20?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starlight/bootstrap/SecurityConfig.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index e47c53d3..6744acd0 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -77,6 +77,9 @@ public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Excep ); } + String officeRedirectUrl = officeBaseUrl.endsWith("/") + ? officeBaseUrl + : officeBaseUrl + "/"; http.securityMatcher("/v1/backoffice/mail/**", "/login", "/logout") .cors(Customizer.withDefaults()) .csrf((csrf) -> csrf @@ -90,22 +93,9 @@ public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Excep .requestMatchers("/login", "/logout").permitAll() .anyRequest().hasRole("BACKOFFICE") ) - .formLogin((form) -> form.successHandler((request, response, authentication) -> { - String proto = request.getHeader("X-Forwarded-Proto"); - if (proto == null || proto.isBlank()) { - proto = request.getScheme(); - } - String host = request.getHeader("X-Forwarded-Host"); - if (host == null || host.isBlank()) { - host = request.getServerName(); - } - String port = request.getHeader("X-Forwarded-Port"); - String portPart = ""; - if (port != null && !port.isBlank() && !"443".equals(port) && !"80".equals(port)) { - portPart = ":" + port; - } - response.sendRedirect(proto + "://" + host + portPart + "/"); - })) + .formLogin((form) -> form.successHandler( + (request, response, authentication) -> response.sendRedirect(officeRedirectUrl) + )) .logout(Customizer.withDefaults()); return http.build(); From 1677d6c8a01c697ea54e8b06e646fbc15c0efae3 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Mon, 19 Jan 2026 10:44:00 +0900 Subject: [PATCH 16/17] =?UTF-8?q?[SRLT-132]=20Refactor:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- .../backoffice/mail/email/SmtpMailSender.java | 4 +++- .../request/BackofficeMailSendRequest.java | 16 ++++++++++++++++ .../starlight/bootstrap/SecurityConfig.java | 19 ++++++++----------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/config b/config index a9d98d9b..d6991821 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit a9d98d9b2ad05b4d753fd0abeb264508317418c3 +Subproject commit d69918214bb7bdb96031e7c45d3cbdef1aec1834 diff --git a/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java b/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java index 47e3117d..5f635a2c 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java +++ b/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java @@ -10,6 +10,8 @@ import org.springframework.stereotype.Service; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; import starlight.application.backoffice.mail.required.MailSenderPort; +import starlight.domain.backoffice.exception.BackofficeErrorType; +import starlight.domain.backoffice.exception.BackofficeException; import starlight.domain.backoffice.mail.BackofficeMailContentType; @Slf4j @@ -40,7 +42,7 @@ public void send(BackofficeMailSendInput input, BackofficeMailContentType conten log.info("[MAIL] sent to={} subject={}", input.to(), input.subject()); } catch (MessagingException e) { log.error("[MAIL] send failed to={}", input.to(), e); - throw new IllegalArgumentException("메일 전송 실패"); + throw new BackofficeException(BackofficeErrorType.MAIL_SEND_FAILED); } } } diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java index 24b17631..fe6dff01 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java @@ -1,8 +1,10 @@ package starlight.adapter.backoffice.mail.webapi.dto.request; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import org.springframework.util.StringUtils; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; import java.util.List; @@ -17,6 +19,20 @@ public record BackofficeMailSendRequest( String html, String text ) { + @AssertTrue(message = "html is required for html contentType; text is required for text contentType") + public boolean isBodyProvided() { + if (!StringUtils.hasText(contentType)) { + return true; + } + if ("html".equalsIgnoreCase(contentType)) { + return StringUtils.hasText(html); + } + if ("text".equalsIgnoreCase(contentType)) { + return StringUtils.hasText(text); + } + return true; + } + public BackofficeMailSendInput toInput() { return BackofficeMailSendInput.of(to, subject, contentType, html, text); } diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index 6744acd0..07f42329 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -25,7 +25,6 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; @@ -53,7 +52,7 @@ public class SecurityConfig { @Value("${cors.origin.office}") String officeBaseUrl; @Value("${cors.origin.develop}") String devBaseUrl; @Value("${backoffice.auth.username}") String backofficeUsername; - @Value("${backoffice.auth.password}") String backofficePassword; + @Value("${backoffice.auth.password-hash}") String backofficePasswordHash; private final Environment environment; private final JwtFilter jwtFilter; @@ -77,15 +76,11 @@ public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Excep ); } - String officeRedirectUrl = officeBaseUrl.endsWith("/") - ? officeBaseUrl - : officeBaseUrl + "/"; http.securityMatcher("/v1/backoffice/mail/**", "/login", "/logout") .cors(Customizer.withDefaults()) .csrf((csrf) -> csrf .csrfTokenRepository(csrfTokenRepository) .csrfTokenRequestHandler(csrfTokenRequestHandler) - .ignoringRequestMatchers("/login", "/logout") ) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .authorizeHttpRequests((authorize) -> @@ -93,9 +88,7 @@ public SecurityFilterChain backofficeFilterChain(HttpSecurity http) throws Excep .requestMatchers("/login", "/logout").permitAll() .anyRequest().hasRole("BACKOFFICE") ) - .formLogin((form) -> form.successHandler( - (request, response, authentication) -> response.sendRedirect(officeRedirectUrl) - )) + .formLogin(Customizer.withDefaults()) .logout(Customizer.withDefaults()); return http.build(); @@ -178,10 +171,13 @@ public PasswordEncoder passwordEncoder() { } @Bean - public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { + public UserDetailsService userDetailsService() { + if (backofficePasswordHash == null || backofficePasswordHash.isBlank()) { + throw new IllegalStateException("backoffice.auth.password-hash must be configured"); + } UserDetails user = User.builder() .username(backofficeUsername) - .password(passwordEncoder.encode(backofficePassword)) + .password(backofficePasswordHash) .roles("BACKOFFICE") .build(); return new InMemoryUserDetailsManager(user); @@ -202,4 +198,5 @@ public AuthenticationProvider authenticationProvider( public LogoutSuccessHandler logoutSuccessHandler() { return new HttpStatusReturningLogoutSuccessHandler(); } + } From b70d0a12cfb5b85a67f53c94b780d69e9ae30ca6 Mon Sep 17 00:00:00 2001 From: seongho5356 Date: Mon, 19 Jan 2026 11:44:39 +0900 Subject: [PATCH 17/17] =?UTF-8?q?[SRLT-132]=20Refactor:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/backoffice/mail/email/SmtpMailSender.java | 2 +- .../mail/webapi/dto/request/BackofficeMailSendRequest.java | 2 ++ .../domain/backoffice/exception/BackofficeException.java | 4 ++++ .../shared/apiPayload/exception/GlobalException.java | 7 ++++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java b/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java index 5f635a2c..c5c3d6f1 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java +++ b/src/main/java/starlight/adapter/backoffice/mail/email/SmtpMailSender.java @@ -42,7 +42,7 @@ public void send(BackofficeMailSendInput input, BackofficeMailContentType conten log.info("[MAIL] sent to={} subject={}", input.to(), input.subject()); } catch (MessagingException e) { log.error("[MAIL] send failed to={}", input.to(), e); - throw new BackofficeException(BackofficeErrorType.MAIL_SEND_FAILED); + throw new BackofficeException(BackofficeErrorType.MAIL_SEND_FAILED, e); } } } diff --git a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java index fe6dff01..616df1cf 100644 --- a/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java +++ b/src/main/java/starlight/adapter/backoffice/mail/webapi/dto/request/BackofficeMailSendRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import org.springframework.util.StringUtils; import starlight.application.backoffice.mail.provided.dto.input.BackofficeMailSendInput; @@ -15,6 +16,7 @@ public record BackofficeMailSendRequest( @NotBlank(message = "subject is required") String subject, @NotBlank(message = "contentType is required") + @Pattern(regexp = "(?i)^(html|text)$", message = "contentType must be html or text") String contentType, String html, String text diff --git a/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java b/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java index 19bf61b7..008b2d87 100644 --- a/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java +++ b/src/main/java/starlight/domain/backoffice/exception/BackofficeException.java @@ -8,4 +8,8 @@ public class BackofficeException extends GlobalException { public BackofficeException(ErrorType errorType) { super(errorType); } + + public BackofficeException(ErrorType errorType, Throwable cause) { + super(errorType, cause); + } } diff --git a/src/main/java/starlight/shared/apiPayload/exception/GlobalException.java b/src/main/java/starlight/shared/apiPayload/exception/GlobalException.java index 1c7a8198..7ecbd40f 100644 --- a/src/main/java/starlight/shared/apiPayload/exception/GlobalException.java +++ b/src/main/java/starlight/shared/apiPayload/exception/GlobalException.java @@ -11,4 +11,9 @@ public GlobalException(ErrorType errorType) { super(errorType.getMessage()); this.errorType = errorType; } -} \ No newline at end of file + + public GlobalException(ErrorType errorType, Throwable cause) { + super(errorType.getMessage(), cause); + this.errorType = errorType; + } +}