diff --git a/build.gradle.kts b/build.gradle.kts index 63ecc1b2..72deccf3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,7 +41,7 @@ subprojects { } } - if (project.name == "cakk-admin") { + if (project.name == "cakk-admin" || project.name == "cakk-external") { apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.plugin.spring") apply(plugin = "org.jetbrains.kotlin.plugin.jpa") @@ -114,6 +114,7 @@ subprojects { "com.cakk.api.provider.oauth.PublicKeyProvider", "com.cakk.api.dto.**", "com.cakk.api.mapper.**", + "com.cakk.api.listener.**", "com.cakk.api.vo.**", "com.cakk.domain.**" ) diff --git a/cakk-api/build.gradle b/cakk-api/build.gradle deleted file mode 100644 index d66fec62..00000000 --- a/cakk-api/build.gradle +++ /dev/null @@ -1,51 +0,0 @@ -description = "api module" - -dependencies { - implementation project(':cakk-common') - implementation project(':cakk-domain:mysql') - implementation project(':cakk-domain:redis') - implementation project(':cakk-client') - implementation project(':cakk-external') - - // basic - implementation('org.springframework.boot:spring-boot-starter-web') - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation("org.springframework.boot:spring-boot-starter-aop:3.3.0") - implementation('org.springframework:spring-tx') - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" - - // Security & OAuth - implementation('org.springframework.boot:spring-boot-starter-security') - implementation('org.springframework.boot:spring-boot-starter-oauth2-client') - implementation('com.google.api-client:google-api-client-jackson2:2.2.0') - implementation('com.google.api-client:google-api-client:2.2.0') - - // Jwt - implementation('io.jsonwebtoken:jjwt-api:0.11.5') - implementation('io.jsonwebtoken:jjwt-impl:0.11.5') - implementation('io.jsonwebtoken:jjwt-jackson:0.11.5') - - // test - testImplementation('com.tngtech.archunit:archunit-junit5:1.1.0') - testImplementation('org.springframework.boot:spring-boot-starter-test') - testImplementation('org.springframework.security:spring-security-test') - testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.23") - - // test container - testImplementation "org.testcontainers:junit-jupiter:1.19.7" - testImplementation "org.testcontainers:mysql:1.19.7" - - // slack 설정 - implementation 'net.gpedro.integrations.slack:slack-webhook:1.4.0' - - // Point - implementation 'org.locationtech.jts:jts-core:1.18.2' -} - -tasks.named("bootJar") { - enabled = true -} - -tasks.named("jar") { - enabled = false -} diff --git a/cakk-api/build.gradle.kts b/cakk-api/build.gradle.kts new file mode 100644 index 00000000..4e191493 --- /dev/null +++ b/cakk-api/build.gradle.kts @@ -0,0 +1,48 @@ +description = "api module" + +dependencies { + implementation(project(":cakk-common")) + implementation(project(":cakk-domain:mysql")) + implementation(project(":cakk-domain:redis")) + implementation(project(":cakk-client")) + implementation(project(":cakk-external")) + + // basic + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-aop:3.3.0") + implementation("org.springframework:spring-tx") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + + // Security & OAuth + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("com.google.api-client:google-api-client-jackson2:2.2.0") + implementation("com.google.api-client:google-api-client:2.2.0") + + // Jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + implementation("io.jsonwebtoken:jjwt-impl:0.11.5") + implementation("io.jsonwebtoken:jjwt-jackson:0.11.5") + + // test + testImplementation("com.tngtech.archunit:archunit-junit5:1.1.0") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.23") + + // test container + testImplementation("org.testcontainers:junit-jupiter:1.19.7") + testImplementation("org.testcontainers:mysql:1.19.7") + + // Point + implementation("org.locationtech.jts:jts-core:1.18.2") +} + +tasks.bootJar { + enabled = true +} + +tasks.jar { + enabled = false +} diff --git a/cakk-api/src/main/java/com/cakk/api/config/CertificationTemplateConfig.java b/cakk-api/src/main/java/com/cakk/api/config/CertificationTemplateConfig.java deleted file mode 100644 index aecefa13..00000000 --- a/cakk-api/src/main/java/com/cakk/api/config/CertificationTemplateConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.cakk.api.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import net.gpedro.integrations.slack.SlackApi; - -import com.cakk.api.template.CertificationTemplate; -import com.cakk.external.executor.CertificationApiExecutor; -import com.cakk.external.executor.CertificationSlackApiExecutor; -import com.cakk.external.extractor.CertificationMessageExtractor; -import com.cakk.external.extractor.CertificationSlackMessageExtractor; - -@Configuration -public class CertificationTemplateConfig { - - private final SlackApi slackApi; - private final boolean isEnable; - - public CertificationTemplateConfig( - SlackApi slackApi, - @Value("${slack.webhook.is-enable}") - boolean isEnable) { - this.slackApi = slackApi; - this.isEnable = isEnable; - } - - @Bean - public CertificationTemplate certificationTemplate() { - return new CertificationTemplate(certificationApiExecutor(), certificationMessageExtractor()); - } - - @Bean - public CertificationApiExecutor certificationApiExecutor() { - return new CertificationSlackApiExecutor(slackApi, isEnable); - } - - @Bean - CertificationMessageExtractor certificationMessageExtractor() { - return new CertificationSlackMessageExtractor(); - } -} diff --git a/cakk-api/src/main/java/com/cakk/api/config/MessageTemplateConfig.java b/cakk-api/src/main/java/com/cakk/api/config/MessageTemplateConfig.java new file mode 100644 index 00000000..168c1eca --- /dev/null +++ b/cakk-api/src/main/java/com/cakk/api/config/MessageTemplateConfig.java @@ -0,0 +1,28 @@ +package com.cakk.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.cakk.external.extractor.CertificationSlackMessageExtractor; +import com.cakk.external.extractor.ErrorAlertSlackMessageExtractor; +import com.cakk.external.extractor.MessageExtractor; +import com.cakk.external.template.MessageTemplate; + +@Configuration +public class MessageTemplateConfig { + + @Bean + public MessageTemplate certificationTemplate() { + return new MessageTemplate(); + } + + @Bean + public MessageExtractor certificationMessageExtractor() { + return new CertificationSlackMessageExtractor(); + } + + @Bean + public MessageExtractor errorAlertMessageExtractor() { + return new ErrorAlertSlackMessageExtractor(); + } +} diff --git a/cakk-api/src/main/java/com/cakk/api/config/SlackWebhookConfig.java b/cakk-api/src/main/java/com/cakk/api/config/SlackWebhookConfig.java deleted file mode 100644 index 9d3df2f4..00000000 --- a/cakk-api/src/main/java/com/cakk/api/config/SlackWebhookConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.cakk.api.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import net.gpedro.integrations.slack.SlackApi; - -@Configuration -public class SlackWebhookConfig { - - private final String slackWebhookUrl; - - public SlackWebhookConfig(@Value("${slack.webhook.url}") String slackWebhookUrl) { - this.slackWebhookUrl = slackWebhookUrl; - } - - @Bean - public SlackApi slackApi() { - return new SlackApi(slackWebhookUrl); - } -} diff --git a/cakk-api/src/main/java/com/cakk/api/controller/advice/GlobalControllerAdvice.java b/cakk-api/src/main/java/com/cakk/api/controller/advice/GlobalControllerAdvice.java index c2757e38..c31b5766 100644 --- a/cakk-api/src/main/java/com/cakk/api/controller/advice/GlobalControllerAdvice.java +++ b/cakk-api/src/main/java/com/cakk/api/controller/advice/GlobalControllerAdvice.java @@ -9,6 +9,8 @@ import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -20,27 +22,37 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.cakk.api.service.slack.SlackService; +import com.cakk.api.mapper.EventMapper; import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.common.response.ApiResponse; @Slf4j @RestControllerAdvice -@RequiredArgsConstructor public class GlobalControllerAdvice { - private final SlackService slackService; + private final ApplicationEventPublisher applicationEventPublisher; + + private final String profile; + + public GlobalControllerAdvice( + ApplicationEventPublisher applicationEventPublisher, + @Value("${spring.profiles.active}") String profile + ) { + this.applicationEventPublisher = applicationEventPublisher; + this.profile = profile; + } @ExceptionHandler(CakkException.class) public ResponseEntity> handleCakkException(CakkException exception, HttpServletRequest request) { final ReturnCode returnCode = exception.getReturnCode(); + if (returnCode.equals(ReturnCode.INTERNAL_SERVER_ERROR) || returnCode.equals(ReturnCode.EXTERNAL_SERVER_ERROR)) { - slackService.sendSlackForError(exception, request); + applicationEventPublisher.publishEvent(EventMapper.supplyErrorAlertEventBy(exception, request, profile)); } + log.error(exception.getMessage()); return getResponseEntity(BAD_REQUEST, ApiResponse.fail(exception.getReturnCode())); } @@ -79,8 +91,9 @@ public ResponseEntity>> handleMethodArgNotValidE RuntimeException.class }) public ResponseEntity> handleServerException(Exception exception, HttpServletRequest request) { - slackService.sendSlackForError(exception, request); + applicationEventPublisher.publishEvent(EventMapper.supplyErrorAlertEventBy(exception, request, profile)); log.error(exception.getMessage()); + return getResponseEntity(INTERNAL_SERVER_ERROR, ApiResponse.error(ReturnCode.INTERNAL_SERVER_ERROR, exception.getMessage())); } diff --git a/cakk-api/src/main/java/com/cakk/api/dto/event/ErrorAlertEvent.java b/cakk-api/src/main/java/com/cakk/api/dto/event/ErrorAlertEvent.java new file mode 100644 index 00000000..6b0845ee --- /dev/null +++ b/cakk-api/src/main/java/com/cakk/api/dto/event/ErrorAlertEvent.java @@ -0,0 +1,10 @@ +package com.cakk.api.dto.event; + +import jakarta.servlet.http.HttpServletRequest; + +public record ErrorAlertEvent( + Exception exception, + HttpServletRequest request, + String profile +) { +} diff --git a/cakk-api/src/main/java/com/cakk/api/listener/CertificationEventListener.java b/cakk-api/src/main/java/com/cakk/api/listener/CertificationEventListener.java index 46dedae9..540a2f0b 100644 --- a/cakk-api/src/main/java/com/cakk/api/listener/CertificationEventListener.java +++ b/cakk-api/src/main/java/com/cakk/api/listener/CertificationEventListener.java @@ -1,27 +1,39 @@ package com.cakk.api.listener; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.scheduling.annotation.Async; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import lombok.RequiredArgsConstructor; - import com.cakk.api.annotation.ApplicationEventListener; import com.cakk.api.mapper.EventMapper; -import com.cakk.api.template.CertificationTemplate; import com.cakk.domain.mysql.event.shop.CertificationEvent; +import com.cakk.external.extractor.MessageExtractor; +import com.cakk.external.sender.MessageSender; +import com.cakk.external.template.MessageTemplate; import com.cakk.external.vo.CertificationMessage; -@RequiredArgsConstructor @ApplicationEventListener public class CertificationEventListener { - private final CertificationTemplate certificationTemplate; + private final MessageTemplate messageTemplate; + private final MessageExtractor messageExtractor; + private final MessageSender messageSender; + + public CertificationEventListener( + MessageTemplate messageTemplate, + @Qualifier("certificationMessageExtractor") MessageExtractor messageExtractor, + @Qualifier("slackMessageSender") MessageSender messageSender + ) { + this.messageTemplate = messageTemplate; + this.messageExtractor = messageExtractor; + this.messageSender = messageSender; + } @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void sendMessageToSlack(CertificationEvent certificationEvent) { CertificationMessage certificationMessage = EventMapper.supplyCertificationMessageBy(certificationEvent); - certificationTemplate.sendMessageForCertification(certificationMessage); + messageTemplate.sendMessage(certificationMessage, messageExtractor, messageSender); } } diff --git a/cakk-api/src/main/java/com/cakk/api/listener/EmailSendEventListener.java b/cakk-api/src/main/java/com/cakk/api/listener/EmailSendEventListener.java index 02f3deeb..49c61a2f 100644 --- a/cakk-api/src/main/java/com/cakk/api/listener/EmailSendEventListener.java +++ b/cakk-api/src/main/java/com/cakk/api/listener/EmailSendEventListener.java @@ -1,25 +1,39 @@ package com.cakk.api.listener; -import java.util.Objects; - +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; -import lombok.RequiredArgsConstructor; - import com.cakk.api.annotation.ApplicationEventListener; import com.cakk.api.dto.event.EmailWithVerificationCodeSendEvent; -import com.cakk.external.service.MailService; +import com.cakk.api.mapper.EventMapper; +import com.cakk.external.extractor.MessageExtractor; +import com.cakk.external.sender.MessageSender; +import com.cakk.external.template.MessageTemplate; +import com.cakk.external.vo.VerificationMessage; -@RequiredArgsConstructor @ApplicationEventListener public class EmailSendEventListener { - private final MailService mailService; + + private final MessageTemplate messageTemplate; + private final MessageExtractor messageExtractor; + private final MessageSender messageSender; + + public EmailSendEventListener( + MessageTemplate messageTemplate, + @Qualifier("verificationCodeMimeMessageExtractor") MessageExtractor messageExtractor, + @Qualifier("emailMessageSender") MessageSender messageSender + ) { + this.messageTemplate = messageTemplate; + this.messageExtractor = messageExtractor; + this.messageSender = messageSender; + } @Async @EventListener public void sendEmailIncludeVerificationCode(EmailWithVerificationCodeSendEvent event) { - mailService.sendEmail(Objects.requireNonNull(event.email()), Objects.requireNonNull(event.code())); + final VerificationMessage verificationMessage = EventMapper.supplyVerificationMessageBy(event); + messageTemplate.sendMessage(verificationMessage, messageExtractor, messageSender); } } diff --git a/cakk-api/src/main/java/com/cakk/api/listener/ErrorAlertEventListener.java b/cakk-api/src/main/java/com/cakk/api/listener/ErrorAlertEventListener.java new file mode 100644 index 00000000..6c0f272b --- /dev/null +++ b/cakk-api/src/main/java/com/cakk/api/listener/ErrorAlertEventListener.java @@ -0,0 +1,39 @@ +package com.cakk.api.listener; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Async; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import com.cakk.api.annotation.ApplicationEventListener; +import com.cakk.api.dto.event.ErrorAlertEvent; +import com.cakk.api.mapper.EventMapper; +import com.cakk.external.extractor.MessageExtractor; +import com.cakk.external.sender.MessageSender; +import com.cakk.external.template.MessageTemplate; +import com.cakk.external.vo.ErrorAlertMessage; + +@ApplicationEventListener +public class ErrorAlertEventListener { + + private final MessageTemplate messageTemplate; + private final MessageExtractor messageExtractor; + private final MessageSender messageSender; + + public ErrorAlertEventListener( + MessageTemplate messageTemplate, + @Qualifier("errorAlertMessageExtractor") MessageExtractor messageExtractor, + @Qualifier("slackMessageSender") MessageSender messageSender + ) { + this.messageTemplate = messageTemplate; + this.messageExtractor = messageExtractor; + this.messageSender = messageSender; + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendMessageToSlack(ErrorAlertEvent errorAlertEvent) { + ErrorAlertMessage certificationMessage = EventMapper.supplyErrorAlertMessageBy(errorAlertEvent); + messageTemplate.sendMessage(certificationMessage, messageExtractor, messageSender); + } +} diff --git a/cakk-api/src/main/java/com/cakk/api/mapper/EventMapper.java b/cakk-api/src/main/java/com/cakk/api/mapper/EventMapper.java index 17eb2af9..e6fd35c0 100644 --- a/cakk-api/src/main/java/com/cakk/api/mapper/EventMapper.java +++ b/cakk-api/src/main/java/com/cakk/api/mapper/EventMapper.java @@ -1,14 +1,20 @@ package com.cakk.api.mapper; +import java.util.Arrays; import java.util.Objects; +import jakarta.servlet.http.HttpServletRequest; + import lombok.AccessLevel; import lombok.NoArgsConstructor; import com.cakk.api.dto.event.EmailWithVerificationCodeSendEvent; +import com.cakk.api.dto.event.ErrorAlertEvent; import com.cakk.api.dto.event.IncreaseSearchCountEvent; import com.cakk.domain.mysql.event.shop.CertificationEvent; import com.cakk.external.vo.CertificationMessage; +import com.cakk.external.vo.ErrorAlertMessage; +import com.cakk.external.vo.VerificationMessage; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class EventMapper { @@ -21,6 +27,14 @@ public static IncreaseSearchCountEvent supplyIncreaseSearchCountEventBy(final St return new IncreaseSearchCountEvent(keyword); } + public static ErrorAlertEvent supplyErrorAlertEventBy( + final Exception exception, + final HttpServletRequest request, + final String profile + ) { + return new ErrorAlertEvent(exception, request, profile); + } + public static CertificationMessage supplyCertificationMessageBy(final CertificationEvent certificationEvent) { Double latitude = null; Double longitude = null; @@ -42,4 +56,25 @@ public static CertificationMessage supplyCertificationMessageBy(final Certificat longitude ); } + + public static VerificationMessage supplyVerificationMessageBy(final EmailWithVerificationCodeSendEvent event) { + return new VerificationMessage(event.email(), event.code()); + } + + public static ErrorAlertMessage supplyErrorAlertMessageBy(final ErrorAlertEvent event) { + final String profile = event.profile(); + final String stackTrace = Arrays.toString(event.exception().getStackTrace()); + final HttpServletRequest request = event.request(); + + return new ErrorAlertMessage( + profile, + stackTrace, + request.getContextPath(), + request.getRequestURL().toString(), + request.getMethod(), + request.getParameterMap(), + request.getRemoteAddr(), + request.getHeader("User-Agent") + ); + } } diff --git a/cakk-api/src/main/java/com/cakk/api/service/slack/SlackService.java b/cakk-api/src/main/java/com/cakk/api/service/slack/SlackService.java deleted file mode 100644 index 053d02bc..00000000 --- a/cakk-api/src/main/java/com/cakk/api/service/slack/SlackService.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.cakk.api.service.slack; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import net.gpedro.integrations.slack.SlackApi; -import net.gpedro.integrations.slack.SlackAttachment; -import net.gpedro.integrations.slack.SlackField; -import net.gpedro.integrations.slack.SlackMessage; - -@Service -public class SlackService { - - private final SlackApi slackApi; - - private final String profile; - private final boolean isEnable; - - public SlackService( - SlackApi slackApi, - @Value("${spring.profiles.active}") - String profile, - @Value("${slack.webhook.is-enable}") - boolean isEnable - ) { - this.slackApi = slackApi; - this.profile = profile; - this.isEnable = isEnable; - } - - public void sendSlackForError(Exception exception, HttpServletRequest request) { - if (!isEnable) { - return; - } - - SlackAttachment slackAttachment = new SlackAttachment(); - slackAttachment.setFallback("Error"); - slackAttachment.setColor("danger"); - slackAttachment.setTitle("Error Detect"); - slackAttachment.setTitleLink(request.getContextPath()); - slackAttachment.setText(Arrays.toString(exception.getStackTrace())); - slackAttachment.setFields( - List.of( - new SlackField().setTitle("Request URL").setValue(request.getRequestURL().toString()), - new SlackField().setTitle("Request Method").setValue(request.getMethod()), - new SlackField().setTitle("Request Parameter").setValue(getRequestParameters(request)), - new SlackField().setTitle("Request Time").setValue(LocalDateTime.now().toString()), - new SlackField().setTitle("Request IP").setValue(request.getRemoteAddr()), - new SlackField().setTitle("Request User-Agent").setValue(request.getHeader("User-Agent")) - ) - ); - - SlackMessage slackMessage = new SlackMessage(); - - slackMessage.setAttachments(List.of(slackAttachment)); - slackMessage.setChannel("#log_server-error"); - slackMessage.setUsername("%s API Error".formatted(profile)); - slackMessage.setIcon(":alert:"); - slackMessage.setText("%s api 에러 발생".formatted(profile)); - - slackApi.call(slackMessage); - } - - private String getRequestParameters(HttpServletRequest request) { - Map parameterMap = request.getParameterMap(); - - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : parameterMap.entrySet()) { - String key = entry.getKey(); - String[] values = entry.getValue(); - - sb.append("Parameter Name: ").append(key); - - for (String value : values) { - sb.append("Parameter Value: ").append(value); - } - } - - return sb.toString(); - } -} diff --git a/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java b/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java index 5581c06f..111c3dfd 100644 --- a/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java +++ b/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java @@ -14,8 +14,8 @@ import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.UserWriter; import com.cakk.domain.redis.repository.TokenRedisRepository; @Service @@ -26,13 +26,13 @@ public class SignService { private final JwtProvider jwtProvider; private final UserReader userReader; - private final UserWriter userWriter; + private final UserCommandFacade userCommandFacade; private final TokenRedisRepository tokenRedisRepository; @Transactional public JwtResponse signUp(final UserSignUpRequest dto) { final String providerId = oidcProviderFactory.getProviderId(dto.provider(), dto.idToken()); - final User user = userWriter.create(UserMapper.supplyUserBy(dto, providerId)); + final User user = userCommandFacade.create(UserMapper.supplyUserBy(dto, providerId)); return JwtResponse.from(jwtProvider.generateToken(user)); } diff --git a/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java b/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java index 13ff7168..96c545f7 100644 --- a/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java +++ b/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java @@ -11,21 +11,15 @@ import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.entity.user.UserWithdrawal; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.BusinessInformationWriter; -import com.cakk.domain.mysql.repository.writer.CakeHeartWriter; -import com.cakk.domain.mysql.repository.writer.CakeShopHeartWriter; -import com.cakk.domain.mysql.repository.writer.UserWriter; @Service @RequiredArgsConstructor public class UserService { private final UserReader userReader; - private final UserWriter userWriter; - private final CakeShopHeartWriter cakeShopHeartWriter; - private final CakeHeartWriter cakeHeartWriter; - private final BusinessInformationWriter businessInformationWriter; + private final UserCommandFacade userCommandFacade; @Transactional(readOnly = true) public ProfileInformationResponse findProfile(final User signInUser) { @@ -39,18 +33,14 @@ public void updateInformation(final User signInUser, final ProfileUpdateRequest final User user = userReader.findByUserId(signInUser.getId()); final ProfileUpdateParam param = UserMapper.supplyProfileUpdateParamBy(dto); - user.updateProfile(param); + userCommandFacade.updateProfile(user, param); } @Transactional public void withdraw(final User signInUser) { - final User user = userReader.findByUserId(signInUser.getId()); + final User user = userReader.findByIdWithAll(signInUser.getId()); final UserWithdrawal withdrawal = UserMapper.supplyUserWithdrawalBy(user); - cakeHeartWriter.deleteAllByUser(user); - cakeShopHeartWriter.deleteAllByUser(user); - businessInformationWriter.deleteAllByUser(user); - - userWriter.delete(user, withdrawal); + userCommandFacade.withdraw(user, withdrawal); } } diff --git a/cakk-api/src/main/java/com/cakk/api/template/CertificationTemplate.java b/cakk-api/src/main/java/com/cakk/api/template/CertificationTemplate.java deleted file mode 100644 index 9b82bbc4..00000000 --- a/cakk-api/src/main/java/com/cakk/api/template/CertificationTemplate.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.cakk.api.template; - -import lombok.RequiredArgsConstructor; - -import com.cakk.external.executor.CertificationApiExecutor; -import com.cakk.external.extractor.CertificationMessageExtractor; -import com.cakk.external.vo.CertificationMessage; - -@RequiredArgsConstructor -public class CertificationTemplate { - - private final CertificationApiExecutor certificationApiExecutor; - private final CertificationMessageExtractor certificationMessageExtractor; - - public void sendMessageForCertification(CertificationMessage certificationMessage) { - this.sendMessageForCertification(certificationMessage, certificationMessageExtractor, certificationApiExecutor); - } - - public void sendMessageForCertification( - CertificationMessage certificationMessage, - CertificationMessageExtractor certificationMessageExtractor, - CertificationApiExecutor certificationApiExecutor - ) { - T extractMessage = (T)certificationMessageExtractor.extract(certificationMessage); - certificationApiExecutor.send(extractMessage); - } -} - diff --git a/cakk-api/src/test/java/com/cakk/api/common/base/MockMvcTest.java b/cakk-api/src/test/java/com/cakk/api/common/base/MockMvcTest.java index 2cafa971..02a1eec3 100644 --- a/cakk-api/src/test/java/com/cakk/api/common/base/MockMvcTest.java +++ b/cakk-api/src/test/java/com/cakk/api/common/base/MockMvcTest.java @@ -25,10 +25,10 @@ import com.cakk.api.controller.shop.ShopController; import com.cakk.api.controller.user.SignController; import com.cakk.api.filter.JwtAuthenticationFilter; +import com.cakk.api.listener.ErrorAlertEventListener; import com.cakk.api.service.like.HeartService; import com.cakk.api.service.like.LikeService; import com.cakk.api.service.shop.ShopService; -import com.cakk.api.service.slack.SlackService; import com.cakk.api.service.user.EmailVerificationService; import com.cakk.api.service.user.SignService; import com.cakk.api.service.views.ViewsService; @@ -56,9 +56,6 @@ public abstract class MockMvcTest { @MockBean protected JwtAuthenticationFilter jwtAuthenticationFilter; - @MockBean - protected SlackService slackService; - @MockBean protected S3Service s3Service; @@ -80,6 +77,9 @@ public abstract class MockMvcTest { @MockBean protected ViewsService viewsService; + @MockBean + protected ErrorAlertEventListener errorAlertEventListener; + @BeforeEach void setup(WebApplicationContext webApplicationContext) { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) diff --git a/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java b/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java index 7af3e991..43e3dc34 100644 --- a/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java @@ -1,5 +1,7 @@ package com.cakk.api.common.base; +import java.time.LocalDate; + import org.junit.jupiter.api.extension.ExtendWith; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; @@ -18,6 +20,7 @@ import com.navercorp.fixturemonkey.jakarta.validation.plugin.JakartaValidationPlugin; import com.cakk.common.enums.Provider; +import com.cakk.common.enums.Role; import com.cakk.domain.mysql.config.JpaConfig; import com.cakk.domain.mysql.entity.user.User; @@ -55,10 +58,14 @@ protected final FixtureMonkey getBuilderMonkey() { } protected User getUser() { - return getReflectionMonkey().giveMeBuilder(User.class) + return getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("nickname", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("birthday", LocalDate.now()) + .set("role", Arbitraries.of(Role.class)) .sample(); } diff --git a/cakk-api/src/test/java/com/cakk/api/controller/GlobalControllerAdviceTest.java b/cakk-api/src/test/java/com/cakk/api/controller/GlobalControllerAdviceTest.java index 0aca3741..5a6a5c86 100644 --- a/cakk-api/src/test/java/com/cakk/api/controller/GlobalControllerAdviceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/controller/GlobalControllerAdviceTest.java @@ -32,7 +32,7 @@ void handleCakkException2() throws Exception { final ReturnCode returnCode = ReturnCode.INTERNAL_SERVER_ERROR; doThrow(new CakkException(returnCode)).when(shopService).searchDetailById(1L); - doNothing().when(slackService).sendSlackForError(any(CakkException.class), any()); + doNothing().when(errorAlertEventListener).sendMessageToSlack(any()); // when & then mockMvc.perform(get("/shops/1")) @@ -40,7 +40,7 @@ void handleCakkException2() throws Exception { .andExpect(jsonPath("$.returnCode").value(returnCode.getCode())) .andExpect(jsonPath("$.returnMessage").value(returnCode.getMessage())); - verify(slackService, times(1)).sendSlackForError(any(CakkException.class), any()); + verify(errorAlertEventListener, times(1)).sendMessageToSlack(any()); } @TestWithDisplayName("CakkException이 발생했고 EXTERNAL_SERVER_ERROR 일 때 BAD_REQUEST를 반환한다.") @@ -49,7 +49,7 @@ void handleCakkException3() throws Exception { final ReturnCode returnCode = ReturnCode.EXTERNAL_SERVER_ERROR; doThrow(new CakkException(returnCode)).when(shopService).searchDetailById(1L); - doNothing().when(slackService).sendSlackForError(any(CakkException.class), any()); + doNothing().when(errorAlertEventListener).sendMessageToSlack(any()); // when & then mockMvc.perform(get("/shops/1")) @@ -57,7 +57,7 @@ void handleCakkException3() throws Exception { .andExpect(jsonPath("$.returnCode").value(returnCode.getCode())) .andExpect(jsonPath("$.returnMessage").value(returnCode.getMessage())); - verify(slackService, times(1)).sendSlackForError(any(CakkException.class), any()); + verify(errorAlertEventListener, times(1)).sendMessageToSlack(any()); } @TestWithDisplayName("HttpMessageNotReadableException이 발생하면 BAD_REQUEST를 반환한다.") diff --git a/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java b/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java index 0c1012b6..a971ab99 100644 --- a/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java +++ b/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java @@ -4,7 +4,6 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.*; import java.time.LocalDate; -import java.util.Map; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -28,11 +27,12 @@ import com.cakk.common.exception.CakkException; import com.cakk.common.response.ApiResponse; import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.reader.UserReader; @SqlGroup({ @Sql(scripts = { - "/sql/insert-test-user.sql" + "/sql/insert-test-user.sql", + "/sql/insert-cake-shop.sql", + "/sql/insert-heart.sql", }, executionPhase = BEFORE_TEST_METHOD), @Sql(scripts = "/sql/delete-all.sql", executionPhase = AFTER_TEST_METHOD) }) diff --git a/cakk-api/src/test/java/com/cakk/api/listener/EmailSendEventListenerTest.java b/cakk-api/src/test/java/com/cakk/api/listener/EmailSendEventListenerTest.java index 27321c39..8b7925c2 100644 --- a/cakk-api/src/test/java/com/cakk/api/listener/EmailSendEventListenerTest.java +++ b/cakk-api/src/test/java/com/cakk/api/listener/EmailSendEventListenerTest.java @@ -11,7 +11,7 @@ import com.cakk.api.common.annotation.TestWithDisplayName; import com.cakk.api.common.base.MockitoTest; import com.cakk.api.dto.event.EmailWithVerificationCodeSendEvent; -import com.cakk.external.service.MailService; +import com.cakk.external.template.MessageTemplate; class EmailSendEventListenerTest extends MockitoTest { @@ -19,7 +19,7 @@ class EmailSendEventListenerTest extends MockitoTest { private EmailSendEventListener emailSendEventListener; @Mock - private MailService mailService; + private MessageTemplate messageTemplate; private EmailWithVerificationCodeSendEvent eventFixture() { return getConstructorMonkey().giveMeBuilder(EmailWithVerificationCodeSendEvent.class) @@ -33,13 +33,13 @@ void sendEmailIncludeVerificationCode() { // given EmailWithVerificationCodeSendEvent event = eventFixture(); - doNothing().when(mailService).sendEmail(event.email(), event.code()); + doNothing().when(messageTemplate).sendMessage(any(), any(), any()); // when assertDoesNotThrow(() -> emailSendEventListener.sendEmailIncludeVerificationCode(event)); // then - verify(mailService, times(1)).sendEmail(event.email(), event.code()); + verify(messageTemplate, times(1)).sendMessage(any(), any(), any()); } @TestWithDisplayName("이벤트에 null 데이터가 포함돼 있으면, 메일 전송 메서드가 호출 시, 에러를 반환한다.") @@ -54,6 +54,6 @@ void sendEmailIncludeVerificationCode2() { ); // then - verify(mailService, times(0)).sendEmail(event.email(), event.code()); + verify(messageTemplate, never()).sendMessage(any(), any(), any()); } } diff --git a/cakk-api/src/test/java/com/cakk/api/listener/ErrorAlertEventListenerTest.java b/cakk-api/src/test/java/com/cakk/api/listener/ErrorAlertEventListenerTest.java new file mode 100644 index 00000000..3a567fcd --- /dev/null +++ b/cakk-api/src/test/java/com/cakk/api/listener/ErrorAlertEventListenerTest.java @@ -0,0 +1,57 @@ +package com.cakk.api.listener; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; + +import com.cakk.api.common.annotation.TestWithDisplayName; +import com.cakk.api.common.base.MockitoTest; +import com.cakk.api.dto.event.ErrorAlertEvent; +import com.cakk.external.template.MessageTemplate; + +class ErrorAlertEventListenerTest extends MockitoTest { + + @InjectMocks + private ErrorAlertEventListener errorAlertEventListener; + + @Mock + private MessageTemplate messageTemplate; + + private HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + + @BeforeEach + void setUp() { + Mockito.when(mockRequest.getContextPath()).thenReturn("/test"); + Mockito.when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/test")); + Mockito.when(mockRequest.getMethod()).thenReturn("GET"); + Mockito.when(mockRequest.getParameterMap()).thenReturn(Map.of("param1", new String[]{"value1"})); + Mockito.when(mockRequest.getRemoteAddr()).thenReturn("127.0.0.1"); + Mockito.when(mockRequest.getHeader("User-Agent")).thenReturn("Mozilla/5.0"); + } + + @TestWithDisplayName("에러 알림 이벤트가 발생하면, 메시지 전송 메서드가 호출되어야 한다.") + void sendMessageToSlack() { + // given + ErrorAlertEvent errorAlertEvent = new ErrorAlertEvent( + new Exception("error"), + mockRequest, + "test" + ); + + doNothing().when(messageTemplate).sendMessage(any(), any(), any()); + + // when + assertDoesNotThrow(() -> errorAlertEventListener.sendMessageToSlack(errorAlertEvent)); + + // then + verify(messageTemplate, times(1)).sendMessage(any(), any(), any()); + } +} diff --git a/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java b/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java index fcdfaf8c..c86a0c0d 100644 --- a/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java @@ -22,8 +22,8 @@ import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.UserWriter; import com.cakk.domain.redis.repository.TokenRedisRepository; @DisplayName("Sign 관련 비즈니스 로직 테스트") @@ -42,7 +42,7 @@ class SignServiceTest extends ServiceTest { private UserReader userReader; @Mock - private UserWriter userWriter; + private UserCommandFacade userCommandFacade; @Mock private TokenRedisRepository tokenRedisRepository; @@ -51,7 +51,7 @@ class SignServiceTest extends ServiceTest { void signUp1() { // given UserSignUpRequest dto = getConstructorMonkey().giveMeOne(UserSignUpRequest.class); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20)) @@ -63,7 +63,7 @@ void signUp1() { .sample(); doReturn(user.getProviderId()).when(oidcProviderFactory).getProviderId(dto.provider(), dto.idToken()); - doReturn(user).when(userWriter).create(any(User.class)); + doReturn(user).when(userCommandFacade).create(any(User.class)); doReturn(jwt).when(jwtProvider).generateToken(user); // when @@ -75,7 +75,7 @@ void signUp1() { Assertions.assertNotNull(result.grantType()); verify(oidcProviderFactory, times(1)).getProviderId(dto.provider(), dto.idToken()); - verify(userWriter, times(1)).create(any(User.class)); + verify(userCommandFacade, times(1)).create(any(User.class)); verify(jwtProvider, times(1)).generateToken(user); } @@ -83,7 +83,7 @@ void signUp1() { void signUp2() { // given UserSignUpRequest dto = getConstructorMonkey().giveMeOne(UserSignUpRequest.class); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20)) @@ -98,7 +98,7 @@ void signUp2() { ReturnCode.EXPIRED_JWT_TOKEN.getMessage()); verify(oidcProviderFactory, times(1)).getProviderId(dto.provider(), dto.idToken()); - verify(userWriter, times(0)).create(any(User.class)); + verify(userCommandFacade, times(0)).create(any(User.class)); verify(jwtProvider, times(0)).generateToken(user); } @@ -107,7 +107,7 @@ void signIn() { // given UserSignInRequest dto = getConstructorMonkey().giveMeOne(UserSignInRequest.class); String providerId = Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20).sample(); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", providerId) @@ -140,7 +140,7 @@ void signIn2() { // given UserSignInRequest dto = getConstructorMonkey().giveMeOne(UserSignInRequest.class); String providerId = Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20).sample(); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", providerId) @@ -164,7 +164,7 @@ void signIn2() { void recreateToken() { // given final String refreshToken = "refresh token"; - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("providerId", Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20)) .set("createdAt", LocalDateTime.now()) diff --git a/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java b/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java index efbcbf1f..5eaf6d6b 100644 --- a/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java @@ -17,11 +17,8 @@ import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.BusinessInformationWriter; -import com.cakk.domain.mysql.repository.writer.CakeHeartWriter; -import com.cakk.domain.mysql.repository.writer.CakeShopHeartWriter; -import com.cakk.domain.mysql.repository.writer.UserWriter; @DisplayName("유저 관련 비즈니스 로직 테스트") class UserServiceTest extends ServiceTest { @@ -33,21 +30,12 @@ class UserServiceTest extends ServiceTest { private UserReader userReader; @Mock - private UserWriter userWriter; - - @Mock - private CakeShopHeartWriter cakeShopHeartWriter; - - @Mock - private CakeHeartWriter cakeHeartWriter; - - @Mock - private BusinessInformationWriter businessInformationWriter; + private UserCommandFacade userCommandFacade; @TestWithDisplayName("유저 프로필을 조회한다.") void findProfile1() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) @@ -67,7 +55,7 @@ void findProfile1() { @TestWithDisplayName("유저가 존재하지 않으면 유저 프로필 조회에 실패한다.") void findProfile2() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) @@ -86,7 +74,7 @@ void findProfile2() { @TestWithDisplayName("유저 프로필을 수정한다.") void updateInformation() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) @@ -104,44 +92,30 @@ void updateInformation() { @TestWithDisplayName("유저를 탈퇴한다.") void withdraw1() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) - .set("id", Arbitraries.longs().greaterOrEqual(10)) - .set("provider", Arbitraries.of(Provider.class)) - .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) - .sample(); + final User user = getUser(); - doReturn(user).when(userReader).findByUserId(user.getId()); + doReturn(user).when(userReader).findByIdWithAll(user.getId()); // when & then Assertions.assertDoesNotThrow(() -> userService.withdraw(user)); - verify(userReader, times(1)).findByUserId(user.getId()); - verify(cakeHeartWriter, times(1)).deleteAllByUser(user); - verify(cakeShopHeartWriter, times(1)).deleteAllByUser(user); - verify(businessInformationWriter, times(1)).deleteAllByUser(user); - verify(userWriter, times(1)).delete(any(), any()); + verify(userReader, times(1)).findByIdWithAll(user.getId()); + verify(userCommandFacade, times(1)).withdraw(any(), any()); } @TestWithDisplayName("유저가 없는 경우, 탈퇴에 실패한다.") void withdraw2() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) - .set("id", Arbitraries.longs().greaterOrEqual(10)) - .set("provider", Arbitraries.of(Provider.class)) - .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) - .sample(); + final User user = getUser(); - doThrow(new CakkException(ReturnCode.NOT_EXIST_USER)).when(userReader).findByUserId(user.getId()); + doThrow(new CakkException(ReturnCode.NOT_EXIST_USER)).when(userReader).findByIdWithAll(user.getId()); // when & then Assertions.assertThrows(CakkException.class, () -> userService.withdraw(user), ReturnCode.NOT_EXIST_USER.getMessage()); - verify(userReader, times(1)).findByUserId(user.getId()); - verify(cakeHeartWriter, times(0)).deleteAllByUser(user); - verify(cakeShopHeartWriter, times(0)).deleteAllByUser(user); - verify(businessInformationWriter, times(0)).deleteAllByUser(user); - verify(userWriter, times(0)).delete(any(), any()); + verify(userReader, times(1)).findByIdWithAll(user.getId()); + verify(userCommandFacade, never()).withdraw(any(), any()); } } diff --git a/cakk-domain/mysql/build.gradle b/cakk-domain/mysql/build.gradle index 289af826..0ce2d0f0 100644 --- a/cakk-domain/mysql/build.gradle +++ b/cakk-domain/mysql/build.gradle @@ -11,6 +11,8 @@ dependencies { testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.23") testImplementation('org.assertj:assertj-core') testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation('org.mockito:mockito-core') + testImplementation('org.mockito:mockito-junit-jupiter') testRuntimeOnly('org.junit.platform:junit-platform-launcher') // querydsl diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java index 0f463261..b843c963 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java @@ -56,7 +56,7 @@ public class BusinessInformation extends AuditEntity { @ColumnDefault("0") @Convert(converter = VerificationStatusConverter.class) @Column(name = "verification_status", nullable = false) - private VerificationStatus verificationStatus = VerificationStatus.UNREQUESTED; + private VerificationStatus verificationStatus; @OneToOne @MapsId @@ -85,6 +85,11 @@ public void updateBusinessOwner(final VerificationPolicy verificationPolicy, fin verificationStatus = verificationPolicy.approveToBusinessOwner(verificationStatus); } + public void unLinkBusinessOwner() { + user = null; + verificationStatus = VerificationStatus.UNREQUESTED; + } + public boolean isBusinessOwnerCandidate(VerificationPolicy verificationPolicy) { return verificationPolicy.isCandidate(Objects.requireNonNull(verificationStatus)); } diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java index e2ae1556..7884c6a7 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java @@ -2,8 +2,11 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -11,10 +14,12 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import org.hibernate.annotations.ColumnDefault; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; @@ -31,7 +36,10 @@ import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; import com.cakk.domain.mysql.entity.audit.AuditEntity; import com.cakk.domain.mysql.entity.cake.Cake; +import com.cakk.domain.mysql.entity.cake.CakeHeart; import com.cakk.domain.mysql.entity.shop.CakeShop; +import com.cakk.domain.mysql.entity.shop.CakeShopHeart; +import com.cakk.domain.mysql.entity.shop.CakeShopLike; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -83,6 +91,22 @@ public class User extends AuditEntity { @Column(name = "deleted_at") private LocalDateTime deletedAt; + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST) + private Set businessInformationSet = new HashSet<>(); + + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set cakeHearts = new HashSet<>(); + + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set cakeShopHearts = new HashSet<>(); + + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set cakeShopLikes = new HashSet<>(); + @Builder public User( Provider provider, @@ -136,6 +160,12 @@ public void unHeartCakeShop(final CakeShop cakeShop) { cakeShop.unHeart(this); } + public void unHeartAndLikeAll() { + cakeHearts.clear(); + cakeShopHearts.clear(); + cakeShopLikes.clear(); + } + @Override public boolean equals(Object object) { if (this == object) { diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/UserWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/facade/user/UserCommandFacade.java similarity index 59% rename from cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/UserWriter.java rename to cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/facade/user/UserCommandFacade.java index e32ba9b5..e1b6b465 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/UserWriter.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/facade/user/UserCommandFacade.java @@ -1,18 +1,20 @@ -package com.cakk.domain.mysql.repository.writer; +package com.cakk.domain.mysql.facade.user; import lombok.RequiredArgsConstructor; import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; -import com.cakk.domain.mysql.annotation.Writer; +import com.cakk.domain.mysql.annotation.DomainFacade; +import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; +import com.cakk.domain.mysql.entity.user.BusinessInformation; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.entity.user.UserWithdrawal; import com.cakk.domain.mysql.repository.jpa.UserJpaRepository; import com.cakk.domain.mysql.repository.jpa.UserWithdrawalJpaRepository; -@Writer @RequiredArgsConstructor -public class UserWriter { +@DomainFacade +public class UserCommandFacade { private final UserJpaRepository userJpaRepository; private final UserWithdrawalJpaRepository userWithdrawalJpaRepository; @@ -26,7 +28,14 @@ public User create(final User user) { return userJpaRepository.save(user); } - public void delete(final User user, final UserWithdrawal withdrawal) { + public void updateProfile(final User user, final ProfileUpdateParam param) { + user.updateProfile(param); + } + + public void withdraw(final User user, final UserWithdrawal withdrawal) { + user.unHeartAndLikeAll(); + user.getBusinessInformationSet().forEach(BusinessInformation::unLinkBusinessOwner); + userWithdrawalJpaRepository.save(withdrawal); userJpaRepository.delete(user); } diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/query/UserQueryRepository.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/query/UserQueryRepository.java new file mode 100644 index 00000000..bbc1f515 --- /dev/null +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/query/UserQueryRepository.java @@ -0,0 +1,33 @@ +package com.cakk.domain.mysql.repository.query; + +import static com.cakk.domain.mysql.entity.user.QUser.*; + +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +import com.cakk.domain.mysql.entity.user.User; + +@Repository +@RequiredArgsConstructor +public class UserQueryRepository { + + private final JPAQueryFactory queryFactory; + + public User searchByIdWithAll(final Long userId) { + return queryFactory.selectFrom(user) + .leftJoin(user.businessInformationSet).fetchJoin() + .leftJoin(user.cakeHearts).fetchJoin() + .leftJoin(user.cakeShopHearts).fetchJoin() + .leftJoin(user.cakeShopLikes).fetchJoin() + .where(eqUserId(userId)) + .fetchOne(); + } + + private BooleanExpression eqUserId(final Long userId) { + return user.id.eq(userId); + } +} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java index a1bd6094..8bc4dd7f 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java @@ -1,5 +1,7 @@ package com.cakk.domain.mysql.repository.reader; +import static java.util.Objects.*; + import java.util.List; import lombok.RequiredArgsConstructor; @@ -9,12 +11,14 @@ import com.cakk.domain.mysql.annotation.Reader; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.repository.jpa.UserJpaRepository; +import com.cakk.domain.mysql.repository.query.UserQueryRepository; @Reader @RequiredArgsConstructor public class UserReader { private final UserJpaRepository userJpaRepository; + private final UserQueryRepository userQueryRepository; public User findByUserId(final Long userId) { return userJpaRepository.findById(userId).orElseThrow(() -> new CakkException(ReturnCode.NOT_EXIST_USER)); @@ -24,6 +28,16 @@ public User findByProviderId(final String providerId) { return userJpaRepository.findByProviderId(providerId).orElseThrow(() -> new CakkException(ReturnCode.NOT_EXIST_USER)); } + public User findByIdWithAll(final Long userId) { + final User user = userQueryRepository.searchByIdWithAll(userId); + + if (isNull(user)) { + throw new CakkException(ReturnCode.NOT_EXIST_USER); + } + + return user; + } + public List findAll() { return userJpaRepository.findAll(); } diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/BusinessInformationWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/BusinessInformationWriter.java deleted file mode 100644 index 20e2393d..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/BusinessInformationWriter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import static java.util.Objects.*; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.user.BusinessInformation; -import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.jpa.BusinessInformationJpaRepository; - -@Writer -@RequiredArgsConstructor -public class BusinessInformationWriter { - - private final BusinessInformationJpaRepository businessInformationJpaRepository; - - public void deleteAllByUser(final User user) { - List businessInformationList = businessInformationJpaRepository.findAllByUser(user); - - if (isNull(businessInformationList) || businessInformationList.isEmpty()) { - return; - } - - businessInformationJpaRepository.deleteAllInBatch(businessInformationList); - } -} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeHeartWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeHeartWriter.java deleted file mode 100644 index 41e0a9e9..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeHeartWriter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import static java.util.Objects.*; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.cake.CakeHeart; -import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.jpa.CakeHeartJpaRepository; - -@Writer -@RequiredArgsConstructor -public class CakeHeartWriter { - - private final CakeHeartJpaRepository cakeHeartJpaRepository; - - public void deleteAllByUser(final User user) { - final List cakeHearts = cakeHeartJpaRepository.findAllByUser(user); - - if (isNull(cakeHearts) || cakeHearts.isEmpty()) { - return; - } - - cakeHeartJpaRepository.deleteAllInBatch(cakeHearts); - } -} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopHeartWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopHeartWriter.java deleted file mode 100644 index 7c35e04f..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopHeartWriter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.shop.CakeShopHeart; -import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.jpa.CakeShopHeartJpaRepository; - -@Writer -@RequiredArgsConstructor -public class CakeShopHeartWriter { - - private final CakeShopHeartJpaRepository cakeShopHeartJpaRepository; - - public void deleteAllByUser(final User user) { - final List cakeShopHearts = cakeShopHeartJpaRepository.findAllByUser(user); - - if (cakeShopHearts == null || cakeShopHearts.isEmpty()) { - return; - } - - cakeShopHeartJpaRepository.deleteAllInBatch(cakeShopHearts); - } -} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopLikeWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopLikeWriter.java deleted file mode 100644 index 486e1a5f..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopLikeWriter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.shop.CakeShop; -import com.cakk.domain.mysql.entity.user.User; - -@Writer -@RequiredArgsConstructor -public class CakeShopLikeWriter { - - public void like(final CakeShop cakeShop, final User user) { - user.likeCakeShop(cakeShop); - } -} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/annotation/TestWithDisplayName.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/annotation/TestWithDisplayName.java new file mode 100644 index 00000000..ff42f323 --- /dev/null +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/annotation/TestWithDisplayName.java @@ -0,0 +1,41 @@ +package com.cakk.domain.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Test +@DisplayNameGeneration(TestWithDisplayName.TestDisplayNameGenerator.class) +public @interface TestWithDisplayName { + + String value() default ""; + + class TestDisplayNameGenerator extends DisplayNameGenerator.Standard { + + @Override + public String generateDisplayNameForClass(Class testClass) { + final TestWithDisplayName testWithDisplayName = testClass.getAnnotation(TestWithDisplayName.class); + + if (testWithDisplayName != null && !testWithDisplayName.value().isEmpty()) { + return testWithDisplayName.value(); + } + + return super.generateDisplayNameForClass(testClass); + } + + @Override + public String generateDisplayNameForMethod(Class testClass, Method testMethod) { + return testMethod.getName(); + } + } +} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java index d02275ba..6c61d86e 100644 --- a/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java @@ -59,7 +59,7 @@ protected static Point supplyPointBy(Double latitude, Double longitude) { } protected User getUserFixture(Role role) { - return getReflectionMonkey().giveMeBuilder(User.class) + return getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) .set("role", role) @@ -94,7 +94,7 @@ protected BusinessInformation getBusinessInformationFixtureWithCakeShop(Verifica } protected CertificationParam getCertificationParamFixtureWithUser(User user) { - return getBuilderMonkey().giveMeBuilder(CertificationParam.class) + return getConstructorMonkey().giveMeBuilder(CertificationParam.class) .set("businessRegistrationImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(20)) .set("idCardImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(20)) .set("cakeShopId", Arbitraries.longs().greaterOrEqual(0)) diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/base/FacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/FacadeTest.java new file mode 100644 index 00000000..9d8942ac --- /dev/null +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/FacadeTest.java @@ -0,0 +1,94 @@ +package com.cakk.domain.base; + +import java.time.LocalDate; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; +import org.mockito.junit.jupiter.MockitoExtension; + +import net.jqwik.api.Arbitraries; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import com.navercorp.fixturemonkey.api.introspector.BuilderArbitraryIntrospector; +import com.navercorp.fixturemonkey.api.introspector.ConstructorPropertiesArbitraryIntrospector; +import com.navercorp.fixturemonkey.api.introspector.FieldReflectionArbitraryIntrospector; +import com.navercorp.fixturemonkey.customizer.Values; +import com.navercorp.fixturemonkey.jakarta.validation.plugin.JakartaValidationPlugin; + +import com.cakk.common.enums.Provider; +import com.cakk.common.enums.Role; +import com.cakk.domain.mysql.entity.cake.Cake; +import com.cakk.domain.mysql.entity.shop.CakeShop; +import com.cakk.domain.mysql.entity.user.User; + +@ExtendWith(MockitoExtension.class) +public abstract class FacadeTest { + + private static final int SPATIAL_REFERENCE_IDENTIFIER_NUMBER = 4326; + + private static final GeometryFactory geometryFactory = new GeometryFactory( + new PrecisionModel(), + SPATIAL_REFERENCE_IDENTIFIER_NUMBER + ); + + protected final FixtureMonkey getConstructorMonkey() { + return FixtureMonkey.builder() + .plugin(new JakartaValidationPlugin()) + .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE) + .build(); + } + + protected final FixtureMonkey getReflectionMonkey() { + return FixtureMonkey.builder() + .plugin(new JakartaValidationPlugin()) + .objectIntrospector(FieldReflectionArbitraryIntrospector.INSTANCE) + .build(); + } + + protected final FixtureMonkey getBuilderMonkey() { + return FixtureMonkey.builder() + .plugin(new JakartaValidationPlugin()) + .objectIntrospector(BuilderArbitraryIntrospector.INSTANCE) + .build(); + } + + protected User getUserFixture(final Role role) { + return getConstructorMonkey().giveMeBuilder(User.class) + .set("id", Arbitraries.longs().greaterOrEqual(10)) + .set("provider", Arbitraries.of(Provider.class)) + .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("nickname", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("birthday", LocalDate.now()) + .set("role", role) + .sample(); + } + + protected Cake getCakeFixture() { + return getConstructorMonkey().giveMeBuilder(Cake.class) + .set("cakeImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) + .set("cakeShop", Values.just(getCakeShopFixture())) + .sample(); + } + + protected CakeShop getCakeShopFixture() { + return getConstructorMonkey().giveMeBuilder(CakeShop.class) + .set("shopName", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(30)) + .set("shopBio", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(40)) + .set("shopDescription", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(500)) + .set("likeCount", 0) + .set("heartCount", 0) + .set("location", supplyPointBy( + Arbitraries.doubles().between(-90, 90).sample(), + Arbitraries.doubles().between(-180, 180).sample()) + ) + .sample(); + } + + public static Point supplyPointBy(Double latitude, Double longitude) { + return geometryFactory.createPoint(new Coordinate(longitude, latitude)); + } +} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserCommandFacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserCommandFacadeTest.java new file mode 100644 index 00000000..69cbf4d3 --- /dev/null +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserCommandFacadeTest.java @@ -0,0 +1,113 @@ +package com.cakk.domain.facade.user; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Optional; + +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import net.jqwik.api.Arbitraries; + +import com.cakk.common.enums.Gender; +import com.cakk.common.enums.ReturnCode; +import com.cakk.common.enums.Role; +import com.cakk.common.exception.CakkException; +import com.cakk.domain.annotation.TestWithDisplayName; +import com.cakk.domain.base.FacadeTest; +import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; +import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.entity.user.UserWithdrawal; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; +import com.cakk.domain.mysql.repository.jpa.UserJpaRepository; +import com.cakk.domain.mysql.repository.jpa.UserWithdrawalJpaRepository; + +class UserCommandFacadeTest extends FacadeTest { + + @InjectMocks + private UserCommandFacade userCommandFacade; + + @Mock + private UserJpaRepository userJpaRepository; + + @Mock + private UserWithdrawalJpaRepository userWithdrawalJpaRepository; + + @TestWithDisplayName("유저를 생성한다") + void create() { + // given + final User user = getUserFixture(Role.USER); + + // when + userCommandFacade.create(user); + + // then + verify(userJpaRepository, times(1)).findByProviderId(user.getProviderId()); + verify(userJpaRepository, times(1)).save(user); + } + + @TestWithDisplayName("유저가 이미 있으면 User 생성에 실패한다") + void create2() { + // given + final User user = getUserFixture(Role.USER); + + doReturn(Optional.of(user)).when(userJpaRepository).findByProviderId(user.getProviderId()); + + // when + assertThrows( + CakkException.class, + () -> userCommandFacade.create(user), + ReturnCode.ALREADY_EXIST_USER.getMessage() + ); + + // then + verify(userJpaRepository, times(1)).findByProviderId(user.getProviderId()); + verify(userJpaRepository, never()).save(user); + } + + @TestWithDisplayName("유저 정보를 수정한다") + void updateProfile() { + // given + final User user = getUserFixture(Role.USER); + final ProfileUpdateParam param = getConstructorMonkey().giveMeBuilder(ProfileUpdateParam.class) + .set("profileImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(100)) + .set("nickname", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(30)) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) + .set("gender", Arbitraries.of(Gender.class)) + .set("birthday", LocalDate.now()) + .sample(); + + // when + userCommandFacade.updateProfile(user, param); + + // then + assertEquals(param.profileImageUrl(), user.getProfileImageUrl()); + assertEquals(param.nickname(), user.getNickname()); + assertEquals(param.email(), user.getEmail()); + assertEquals(param.gender(), user.getGender()); + assertEquals(param.birthday(), user.getBirthday()); + } + + @TestWithDisplayName("유저를 탈퇴한다") + void withdraw() { + // given + final User user = getUserFixture(Role.USER); + final UserWithdrawal withdrawal = getConstructorMonkey().giveMeBuilder(UserWithdrawal.class) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) + .set("gender", Arbitraries.of(Gender.class)) + .set("birthday", LocalDate.now()) + .set("role", Arbitraries.of(Role.class)) + .set("withdrawalDate", LocalDateTime.now()) + .sample(); + + // when + userCommandFacade.withdraw(user, withdrawal); + + // then + verify(userWithdrawalJpaRepository, times(1)).save(any()); + verify(userJpaRepository, times(1)).delete(user); + } +} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java index 4bd57d61..2ab46c38 100644 --- a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java @@ -4,17 +4,19 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import com.cakk.common.enums.Role; -import com.cakk.domain.base.DomainTest; +import com.cakk.domain.base.FacadeTest; import com.cakk.domain.mysql.entity.cake.Cake; import com.cakk.domain.mysql.entity.shop.CakeShop; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.facade.user.UserHeartFacade; -class UserHeartFacadeTest extends DomainTest { +class UserHeartFacadeTest extends FacadeTest { - private final UserHeartFacade userHeartFacade = new UserHeartFacade(); + @InjectMocks + private UserHeartFacade userHeartFacade; @Test @DisplayName("케이크 하트를 성공한다") diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java index 27dfc58d..08ccf240 100644 --- a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java @@ -7,18 +7,20 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import com.cakk.common.enums.ReturnCode; import com.cakk.common.enums.Role; import com.cakk.common.exception.CakkException; -import com.cakk.domain.base.DomainTest; +import com.cakk.domain.base.FacadeTest; import com.cakk.domain.mysql.entity.shop.CakeShop; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.facade.user.UserLikeFacade; -class UserLikeFacadeTest extends DomainTest { +class UserLikeFacadeTest extends FacadeTest { - private final UserLikeFacade userLikeFacade = new UserLikeFacade(); + @InjectMocks + private UserLikeFacade userLikeFacade; @Test @DisplayName("케이크 샵 기대돼요 동작을 성공한다") diff --git a/cakk-external/build.gradle b/cakk-external/build.gradle deleted file mode 100644 index caf74ef3..00000000 --- a/cakk-external/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -description = "external module" - - -dependencies { - implementation project(':cakk-common') - - // Basic - implementation('org.springframework:spring-context') - - // AWS - implementation('com.amazonaws:aws-java-sdk-s3:1.12.715') - - // Mail - implementation('org.springframework.boot:spring-boot-starter-mail') - - // Slack - implementation 'net.gpedro.integrations.slack:slack-webhook:1.4.0' -} - - -bootJar { - enabled = false -} - -jar { - enabled = true -} diff --git a/cakk-external/build.gradle.kts b/cakk-external/build.gradle.kts new file mode 100644 index 00000000..422496a7 --- /dev/null +++ b/cakk-external/build.gradle.kts @@ -0,0 +1,26 @@ +description = "external module" + +dependencies { + implementation(project(":cakk-common")) + + // Basic + implementation("org.springframework:spring-context") + + // AWS + implementation("com.amazonaws:aws-java-sdk-s3:1.12.715") + + // Mail + implementation("org.springframework.boot:spring-boot-starter-mail") + + // Slack + implementation("net.gpedro.integrations.slack:slack-webhook:1.4.0") +} + + +tasks.bootJar { + enabled = false +} + +tasks.jar { + enabled = true +} diff --git a/cakk-external/src/main/java/com/cakk/external/config/EmailConfig.kt b/cakk-external/src/main/java/com/cakk/external/config/EmailConfig.kt new file mode 100644 index 00000000..f084a29f --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/config/EmailConfig.kt @@ -0,0 +1,61 @@ +package com.cakk.external.config + +import java.util.* + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.JavaMailSenderImpl + +@Configuration +class EmailConfig( + @Value("\${spring.mail.host}") + private val host: String, + @Value("\${spring.mail.port}") + private val port: Int, + @Value("\${spring.mail.username}") + private val username: String, + @Value("\${spring.mail.password}") + private val password: String, + @Value("\${spring.mail.properties.mail.smtp.auth}") + private val auth: Boolean, + @Value("\${spring.mail.properties.mail.smtp.starttls.enable}") + private val starttlsEnable: Boolean, + @Value("\${spring.mail.properties.mail.smtp.starttls.required}") + private val starttlsRequired: Boolean, + @Value("\${spring.mail.properties.mail.smtp.connectiontimeout}") + private val connectionTimeout: Int, + @Value("\${spring.mail.properties.mail.smtp.timeout}") + private val timeout: Int, + @Value("\${spring.mail.properties.mail.smtp.writetimeout}") + private val writeTimeout: Int +) { + + @Bean + fun javaMailSender(): JavaMailSender { + val mailSender = JavaMailSenderImpl() + + mailSender.host = host + mailSender.port = port + mailSender.username = username + mailSender.password = password + mailSender.defaultEncoding = "UTF-8" + mailSender.javaMailProperties = mailProperties() + + return mailSender + } + + private fun mailProperties(): Properties { + val properties = Properties() + + properties["mail.smtp.auth"] = auth + properties["mail.smtp.starttls.enable"] = starttlsEnable + properties["mail.smtp.starttls.required"] = starttlsRequired + properties["mail.smtp.connectiontimeout"] = connectionTimeout + properties["mail.smtp.timeout"] = timeout + properties["mail.smtp.writetimeout"] = writeTimeout + + return properties + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/config/MailConfig.java b/cakk-external/src/main/java/com/cakk/external/config/MailConfig.java deleted file mode 100644 index 6552c02c..00000000 --- a/cakk-external/src/main/java/com/cakk/external/config/MailConfig.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.cakk.external.config; - -import java.util.Properties; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.JavaMailSenderImpl; - -@Configuration -public class MailConfig { - - private final String host; - private final int port; - private final String username; - private final String password; - private final boolean auth; - private final boolean starttlsEnable; - private final boolean starttlsRequired; - private final int connectionTimeout; - private final int timeout; - private final int writeTimeout; - - public MailConfig( - @Value("${spring.mail.host}") String host, - @Value("${spring.mail.port}") int port, - @Value("${spring.mail.username}") String username, - @Value("${spring.mail.password}") String password, - @Value("${spring.mail.properties.mail.smtp.auth}") boolean auth, - @Value("${spring.mail.properties.mail.smtp.starttls.enable}") boolean starttlsEnable, - @Value("${spring.mail.properties.mail.smtp.starttls.required}") boolean starttlsRequired, - @Value("${spring.mail.properties.mail.smtp.connectiontimeout}") int connectionTimeout, - @Value("${spring.mail.properties.mail.smtp.timeout}") int timeout, - @Value("${spring.mail.properties.mail.smtp.writetimeout}") int writeTimeout - ) { - this.host = host; - this.port = port; - this.username = username; - this.password = password; - this.auth = auth; - this.starttlsEnable = starttlsEnable; - this.starttlsRequired = starttlsRequired; - this.connectionTimeout = connectionTimeout; - this.timeout = timeout; - this.writeTimeout = writeTimeout; - } - - @Bean - public JavaMailSender javaMailSender() { - JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); - mailSender.setHost(host); - mailSender.setPort(port); - mailSender.setUsername(username); - mailSender.setPassword(password); - mailSender.setDefaultEncoding("UTF-8"); - mailSender.setJavaMailProperties(getMailProperties()); - - return mailSender; - } - - private Properties getMailProperties() { - Properties properties = new Properties(); - properties.put("mail.smtp.auth", auth); - properties.put("mail.smtp.starttls.enable", starttlsEnable); - properties.put("mail.smtp.starttls.required", starttlsRequired); - properties.put("mail.smtp.connectiontimeout", connectionTimeout); - properties.put("mail.smtp.timeout", timeout); - properties.put("mail.smtp.writetimeout", writeTimeout); - - return properties; - } -} diff --git a/cakk-external/src/main/java/com/cakk/external/config/SlackWebhookConfig.kt b/cakk-external/src/main/java/com/cakk/external/config/SlackWebhookConfig.kt new file mode 100644 index 00000000..eb0eafbf --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/config/SlackWebhookConfig.kt @@ -0,0 +1,19 @@ +package com.cakk.external.config + +import net.gpedro.integrations.slack.SlackApi + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class SlackWebhookConfig( + @Value("\${slack.webhook.url}") + private val slackWebhookUrl: String +) { + + @Bean + fun slackApi(): SlackApi { + return SlackApi(slackWebhookUrl) + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/executor/CertificationApiExecutor.java b/cakk-external/src/main/java/com/cakk/external/executor/CertificationApiExecutor.java deleted file mode 100644 index b170a6de..00000000 --- a/cakk-external/src/main/java/com/cakk/external/executor/CertificationApiExecutor.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.cakk.external.executor; - -public interface CertificationApiExecutor { - void send(T message); -} - diff --git a/cakk-external/src/main/java/com/cakk/external/executor/CertificationSlackApiExecutor.java b/cakk-external/src/main/java/com/cakk/external/executor/CertificationSlackApiExecutor.java deleted file mode 100644 index bbb424c8..00000000 --- a/cakk-external/src/main/java/com/cakk/external/executor/CertificationSlackApiExecutor.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.cakk.external.executor; - -import net.gpedro.integrations.slack.SlackApi; -import net.gpedro.integrations.slack.SlackMessage; - -public class CertificationSlackApiExecutor implements CertificationApiExecutor { - - private final SlackApi slackApi; - private final boolean isEnable; - - public CertificationSlackApiExecutor( - SlackApi slackApi, - boolean isEnable - ) { - this.slackApi = slackApi; - this.isEnable = isEnable; - } - - @Override - public void send(SlackMessage message) { - if (!isEnable) { - return; - } - - slackApi.call(message); - } -} - diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/CertificationMessageExtractor.java b/cakk-external/src/main/java/com/cakk/external/extractor/CertificationMessageExtractor.java deleted file mode 100644 index 9f88c59e..00000000 --- a/cakk-external/src/main/java/com/cakk/external/extractor/CertificationMessageExtractor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.cakk.external.extractor; - -import com.cakk.external.vo.CertificationMessage; - -public interface CertificationMessageExtractor { - T extract(CertificationMessage certificationMessage); -} - diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/CertificationSlackMessageExtractor.java b/cakk-external/src/main/java/com/cakk/external/extractor/CertificationSlackMessageExtractor.java deleted file mode 100644 index b5ee8b99..00000000 --- a/cakk-external/src/main/java/com/cakk/external/extractor/CertificationSlackMessageExtractor.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.cakk.external.extractor; - -import java.util.List; - -import net.gpedro.integrations.slack.SlackAttachment; -import net.gpedro.integrations.slack.SlackField; -import net.gpedro.integrations.slack.SlackMessage; - -import com.cakk.external.vo.CertificationMessage; - -public class CertificationSlackMessageExtractor implements CertificationMessageExtractor { - - @Override - public SlackMessage extract(CertificationMessage certificationMessage) { - SlackAttachment slackAttachment = new SlackAttachment(); - slackAttachment.setColor("good"); - slackAttachment.setFallback("OK"); - slackAttachment.setTitle("Request Certification"); - - slackAttachment.setFields(List.of( - new SlackField().setTitle("요청자 PK").setValue(String.valueOf(certificationMessage.userId())), - new SlackField().setTitle("요청자 이메일").setValue(certificationMessage.userEmail()), - new SlackField().setTitle("요청자 비상연락망").setValue(certificationMessage.emergencyContact()), - new SlackField().setTitle("요청자 신분증 이미지").setValue(certificationMessage.idCardImageUrl()), - new SlackField().setTitle("요청자 사업자등록증 이미지").setValue(certificationMessage.businessRegistrationImageUrl()), - new SlackField().setTitle("요청 사항").setValue(certificationMessage.message()), - new SlackField().setTitle("가게 이름").setValue(certificationMessage.shopName()), - new SlackField().setTitle("가게 위치 위도").setValue(String.valueOf(certificationMessage.latitude())), - new SlackField().setTitle("가게 위치 경도").setValue(String.valueOf(certificationMessage.longitude())) - )); - - SlackMessage slackMessage = new SlackMessage(); - slackMessage.setAttachments(List.of(slackAttachment)); - slackMessage.setChannel("#cs_사장님인증"); - slackMessage.setText("사장님 인증 요청"); - - return slackMessage; - } -} - diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/CertificationSlackMessageExtractor.kt b/cakk-external/src/main/java/com/cakk/external/extractor/CertificationSlackMessageExtractor.kt new file mode 100644 index 00000000..19f2a845 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/extractor/CertificationSlackMessageExtractor.kt @@ -0,0 +1,39 @@ +package com.cakk.external.extractor + +import net.gpedro.integrations.slack.SlackAttachment +import net.gpedro.integrations.slack.SlackField +import net.gpedro.integrations.slack.SlackMessage + +import com.cakk.external.vo.CertificationMessage + +class CertificationSlackMessageExtractor : SlackMessageExtractor { + + override fun extract(message: CertificationMessage): SlackMessage { + val slackAttachment = SlackAttachment() + slackAttachment.setColor("good") + slackAttachment.setFallback("OK") + slackAttachment.setTitle("Request Certification") + + slackAttachment.setFields( + listOf( + SlackField().setTitle("요청자 PK").setValue(message.userId.toString()), + SlackField().setTitle("요청자 이메일").setValue(message.userEmail), + SlackField().setTitle("요청자 비상연락망").setValue(message.emergencyContact), + SlackField().setTitle("요청자 신분증 이미지").setValue(message.idCardImageUrl), + SlackField().setTitle("요청자 사업자등록증 이미지").setValue(message.businessRegistrationImageUrl), + SlackField().setTitle("요청 사항").setValue(message.message), + SlackField().setTitle("가게 이름").setValue(message.shopName), + SlackField().setTitle("가게 위치 위도").setValue(message.latitude.toString()), + SlackField().setTitle("가게 위치 경도").setValue(message.longitude.toString()) + ) + ) + + val slackMessage = SlackMessage() + slackMessage.setAttachments(listOf(slackAttachment)) + slackMessage.setText("사장님 인증 요청") + slackMessage.setChannel("#cs_사장님인증") + + return slackMessage + } +} + diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/ErrorAlertSlackMessageExtractor.kt b/cakk-external/src/main/java/com/cakk/external/extractor/ErrorAlertSlackMessageExtractor.kt new file mode 100644 index 00000000..0deca1f8 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/extractor/ErrorAlertSlackMessageExtractor.kt @@ -0,0 +1,56 @@ +package com.cakk.external.extractor + +import com.cakk.external.vo.ErrorAlertMessage +import net.gpedro.integrations.slack.SlackAttachment +import net.gpedro.integrations.slack.SlackField +import net.gpedro.integrations.slack.SlackMessage +import java.time.LocalDateTime + +class ErrorAlertSlackMessageExtractor : SlackMessageExtractor { + + override fun extract(message: ErrorAlertMessage): SlackMessage { + val slackAttachment = SlackAttachment() + slackAttachment.setFallback("Error") + slackAttachment.setColor("danger") + slackAttachment.setTitle("Error Detect") + slackAttachment.setTitleLink(message.contextPath) + slackAttachment.setText(message.stackTrace) + + slackAttachment.setFields( + listOf( + SlackField().setTitle("Request URL").setValue(message.requestURL), + SlackField().setTitle("Request Method").setValue(message.method), + SlackField().setTitle("Request Time").setValue(LocalDateTime.now().toString()), + SlackField().setTitle("Request IP").setValue(message.remoteAddr), + SlackField().setTitle("Request User-Agent").setValue(message.header), + message.parameterMap?.run { + SlackField().setTitle("Request Parameter").setValue(getRequestParameters(message.parameterMap)) + } + ) + ) + + val slackMessage = SlackMessage() + + slackMessage.setAttachments(listOf(slackAttachment)) + slackMessage.setChannel("#log_server-error") + slackMessage.setUsername("${message.serverProfile} API Error") + slackMessage.setIcon(":alert:") + slackMessage.setText("${message.serverProfile} api 에러 발생") + + return slackMessage + } + + private fun getRequestParameters(parameterMap: Map>): String { + val sb = StringBuilder() + + parameterMap.forEach { (key, values) -> + sb.append("Parameter Name: ").append(key) + + values.forEach { + sb.append("Parameter Value: ").append(it) + } + } + + return sb.toString() + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/MessageExtractor.kt b/cakk-external/src/main/java/com/cakk/external/extractor/MessageExtractor.kt new file mode 100644 index 00000000..d375c642 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/extractor/MessageExtractor.kt @@ -0,0 +1,6 @@ +package com.cakk.external.extractor + +fun interface MessageExtractor { + + fun extract(message: T): U +} diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/MimeMessageExtractor.kt b/cakk-external/src/main/java/com/cakk/external/extractor/MimeMessageExtractor.kt new file mode 100644 index 00000000..a3866da0 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/extractor/MimeMessageExtractor.kt @@ -0,0 +1,8 @@ +package com.cakk.external.extractor + +import jakarta.mail.internet.MimeMessage + +interface MimeMessageExtractor : MessageExtractor { + + override fun extract(message: T): MimeMessage +} diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/SlackMessageExtractor.kt b/cakk-external/src/main/java/com/cakk/external/extractor/SlackMessageExtractor.kt new file mode 100644 index 00000000..fe57731a --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/extractor/SlackMessageExtractor.kt @@ -0,0 +1,8 @@ +package com.cakk.external.extractor + +import net.gpedro.integrations.slack.SlackMessage + +fun interface SlackMessageExtractor : MessageExtractor { + + override fun extract(message: T): SlackMessage +} diff --git a/cakk-external/src/main/java/com/cakk/external/extractor/VerificationCodeMimeMessageExtractor.kt b/cakk-external/src/main/java/com/cakk/external/extractor/VerificationCodeMimeMessageExtractor.kt new file mode 100644 index 00000000..04ef2d0e --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/extractor/VerificationCodeMimeMessageExtractor.kt @@ -0,0 +1,50 @@ +package com.cakk.external.extractor + +import jakarta.mail.MessagingException +import jakarta.mail.internet.MimeMessage + +import org.springframework.beans.factory.annotation.Value +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.MimeMessageHelper +import org.springframework.stereotype.Component + +import com.cakk.common.enums.ReturnCode +import com.cakk.common.exception.CakkException +import com.cakk.external.vo.VerificationMessage + +@Component +class VerificationCodeMimeMessageExtractor( + private val mailSender: JavaMailSender, + @Value("\${spring.mail.username}") + private val senderEmail: String +) : MimeMessageExtractor { + + override fun extract(message: VerificationMessage): MimeMessage { + val mimeMessage = mailSender.createMimeMessage() + + try { + val helper = MimeMessageHelper(mimeMessage, true, "UTF-8") + + helper.setTo(message.receiver) + helper.setFrom(senderEmail) + helper.setSubject("[케이크크] 이메일 인증") + + val body = """ +

인증코드를 확인해 주세요.

+

${message.verificationCode}

+ 이메일 인증 절차에 따라 이메일 인증코드를 발급해드립니다.
+ 인증코드는 이메일 발송시점으로부터 3분 동안 유효합니다.
+ 만약 본인 요청에 의한 이메일 인증이 아니라면, cakk.contact@gmail.com 으로 관련 내용을 전달해 주세요. + 더욱 편리한 서비스를 제공하기 위해 항상 최선을 다하는 케이크크가 되겠습니다. +

+ + 감사합니다. + """.trimIndent() + helper.setText(body, true) + } catch (e: MessagingException) { + throw CakkException(ReturnCode.SEND_EMAIL_ERROR) + } + + return mimeMessage + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/sender/EmailMessageSender.kt b/cakk-external/src/main/java/com/cakk/external/sender/EmailMessageSender.kt new file mode 100644 index 00000000..f6788e10 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/sender/EmailMessageSender.kt @@ -0,0 +1,23 @@ +package com.cakk.external.sender + +import jakarta.mail.internet.MimeMessage + +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.stereotype.Component + +import com.cakk.common.enums.ReturnCode +import com.cakk.common.exception.CakkException + +@Component +class EmailMessageSender( + private val mailSender: JavaMailSender +) : MessageSender { + + override fun send(message: MimeMessage) { + try { + mailSender.send(message) + } catch (e: RuntimeException) { + throw CakkException(ReturnCode.SEND_EMAIL_ERROR) + } + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/sender/MessageSender.kt b/cakk-external/src/main/java/com/cakk/external/sender/MessageSender.kt new file mode 100644 index 00000000..9ed7ef2c --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/sender/MessageSender.kt @@ -0,0 +1,6 @@ +package com.cakk.external.sender + +fun interface MessageSender { + + fun send(message: T) +} diff --git a/cakk-external/src/main/java/com/cakk/external/sender/SlackMessageSender.kt b/cakk-external/src/main/java/com/cakk/external/sender/SlackMessageSender.kt new file mode 100644 index 00000000..4420ea27 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/sender/SlackMessageSender.kt @@ -0,0 +1,23 @@ +package com.cakk.external.sender + +import net.gpedro.integrations.slack.SlackApi +import net.gpedro.integrations.slack.SlackMessage + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component + +@Component +class SlackMessageSender( + private val slackApi: SlackApi, + @Value("\${slack.webhook.is-enable}") + private val isEnable: Boolean +): MessageSender { + + override fun send(message: SlackMessage) { + if (!isEnable) { + return + } + + slackApi.call(message) + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/service/MailService.java b/cakk-external/src/main/java/com/cakk/external/service/MailService.java deleted file mode 100644 index 8a266e2f..00000000 --- a/cakk-external/src/main/java/com/cakk/external/service/MailService.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.cakk.external.service; - -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; - -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 com.cakk.common.enums.ReturnCode; -import com.cakk.common.exception.CakkException; - -@Service -public class MailService { - - private final JavaMailSender mailSender; - - private final String senderEmail; - - public MailService( - JavaMailSender mailSender, - @Value("${spring.mail.username}") String username - ) { - this.mailSender = mailSender; - this.senderEmail = username; - } - - public void sendEmail(final String receiverEmail, final String code) { - try { - final MimeMessage emailForm = createMailFrom(receiverEmail, code); - - mailSender.send(emailForm); - } catch (RuntimeException e) { - throw new CakkException(ReturnCode.SEND_EMAIL_ERROR); - } - } - - private MimeMessage createMailFrom(final String receiverMail, final String code) { - final MimeMessage message = mailSender.createMimeMessage(); - - try { - final MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - - helper.setTo(receiverMail); - helper.setFrom(senderEmail); - helper.setSubject("[케이크크] 이메일 인증"); - - String body = """ -

인증코드를 확인해 주세요.

-

%s

- 이메일 인증 절차에 따라 이메일 인증코드를 발급해드립니다.
- 인증코드는 이메일 발송시점으로부터 3분 동안 유효합니다.
- 만약 본인 요청에 의한 이메일 인증이 아니라면, cakk.contact@gmail.com 으로 관련 내용을 전달해 주세요. - 더욱 편리한 서비스를 제공하기 위해 항상 최선을 다하는 케이크크가 되겠습니다. -

- - 감사합니다. - """.formatted(code); - helper.setText(body, true); - } catch (MessagingException e) { - throw new CakkException(ReturnCode.SEND_EMAIL_ERROR); - } - - return message; - } -} diff --git a/cakk-external/src/main/java/com/cakk/external/template/MessageTemplate.kt b/cakk-external/src/main/java/com/cakk/external/template/MessageTemplate.kt new file mode 100644 index 00000000..68e6c791 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/template/MessageTemplate.kt @@ -0,0 +1,16 @@ +package com.cakk.external.template + +import com.cakk.external.extractor.MessageExtractor +import com.cakk.external.sender.MessageSender + +class MessageTemplate { + + fun sendMessage( + message: T, + messageExtractor: MessageExtractor, + messageSender: MessageSender, + ) { + val extractMessage: U = messageExtractor.extract(message) + messageSender.send(extractMessage) + } +} diff --git a/cakk-external/src/main/java/com/cakk/external/vo/ErrorAlertMessage.kt b/cakk-external/src/main/java/com/cakk/external/vo/ErrorAlertMessage.kt new file mode 100644 index 00000000..63a4b1de --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/vo/ErrorAlertMessage.kt @@ -0,0 +1,12 @@ +package com.cakk.external.vo + +data class ErrorAlertMessage( + val serverProfile: String, + val stackTrace: String?, + val contextPath: String?, + val requestURL: String?, + val method: String?, + val parameterMap: Map>?, + val remoteAddr: String?, + val header: String? +) diff --git a/cakk-external/src/main/java/com/cakk/external/vo/VerificationMessage.kt b/cakk-external/src/main/java/com/cakk/external/vo/VerificationMessage.kt new file mode 100644 index 00000000..66988a69 --- /dev/null +++ b/cakk-external/src/main/java/com/cakk/external/vo/VerificationMessage.kt @@ -0,0 +1,6 @@ +package com.cakk.external.vo + +data class VerificationMessage( + val receiver: String, + val verificationCode: String +)