From 5e96109ef07fb736b2cca0dbbdddf8039e43cedf Mon Sep 17 00:00:00 2001 From: dev-Crayon Date: Sun, 28 Jul 2024 14:15:45 +0900 Subject: [PATCH] =?UTF-8?q?[Refactor]:=20=EC=86=8C=EC=85=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20API=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Request body 검증성 추가 비즈니스 로직 캡슐화 에러 처리 Related to: #157 --- build.gradle | 4 ++ .../auth/application/SocialService.java | 47 ++++++++----------- .../auth/application/UserCreator.java | 20 ++++++++ .../auth/application/UserService.java | 2 +- .../application/util/UserServiceUtil.java | 13 +++++ .../SobokSobok/auth/domain/SocialInfo.java | 21 +++++++-- .../io/sobok/SobokSobok/auth/domain/User.java | 12 ++--- .../auth/ui/dto/SocialSignupRequest.java | 12 +++-- .../auth/ui/dto/SocialSignupResponse.java | 9 ++++ .../SobokSobok/common/dto/ApiResponse.java | 4 ++ .../exception/ControllerExceptionAdvice.java | 18 +++++++ .../sobok/SobokSobok/exception/ErrorCode.java | 2 +- .../external/firebase/FCMPushService.java | 4 +- 13 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 src/main/java/io/sobok/SobokSobok/auth/application/UserCreator.java diff --git a/build.gradle b/build.gradle index 3f02ec2..eded228 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,10 @@ dependencies { } +test { + useJUnitPlatform() +} + tasks.named('bootBuildImage') { builder = 'paketobuildpacks/builder-jammy-base:latest' } diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java b/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java index be6c113..ef19c98 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java +++ b/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java @@ -1,15 +1,13 @@ package io.sobok.SobokSobok.auth.application; +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; import io.sobok.SobokSobok.auth.domain.Role; -import io.sobok.SobokSobok.auth.domain.SocialInfo; import io.sobok.SobokSobok.auth.domain.User; import io.sobok.SobokSobok.auth.infrastructure.UserRepository; import io.sobok.SobokSobok.auth.ui.dto.SocialLoginRequest; import io.sobok.SobokSobok.auth.ui.dto.SocialLoginResponse; import io.sobok.SobokSobok.auth.ui.dto.SocialSignupRequest; import io.sobok.SobokSobok.auth.ui.dto.SocialSignupResponse; -import io.sobok.SobokSobok.exception.ErrorCode; -import io.sobok.SobokSobok.exception.model.ConflictException; import io.sobok.SobokSobok.security.jwt.Jwt; import io.sobok.SobokSobok.security.jwt.JwtProvider; @@ -23,39 +21,34 @@ @RequiredArgsConstructor public class SocialService { + private final UserCreator userCreator; + private final UserRepository userRepository; private final JwtProvider jwtProvider; @Transactional public SocialSignupResponse signup(SocialSignupRequest request) { - if (userRepository.existsBySocialInfoSocialId(request.socialId())) { - throw new ConflictException(ErrorCode.ALREADY_EXISTS_USER); - } - - if (userRepository.existsByUsername(request.username())) { - throw new ConflictException(ErrorCode.ALREADY_USING_USERNAME); - } + UserServiceUtil.checkAlreadySignupSocialId(userRepository, request.socialId()); + UserServiceUtil.checkAlreadyUsedNickname(userRepository, request.nickname()); - User signupUser = userRepository.save(User.builder() - .username(request.username()) - .socialInfo(SocialInfo.builder() - .socialId(request.socialId()) - .build()) - .deviceToken(request.deviceToken()) - .roles(Role.USER.name()) - .platform(request.platform()) - .build()); + User signupUser = userCreator.create( + request.nickname(), + request.socialId(), + request.platform(), + request.deviceToken(), + Role.USER.name() + ); Jwt jwt = jwtProvider.getUserJwt(signupUser.getSocialInfo().getSocialId()); - return SocialSignupResponse.builder() - .id(signupUser.getId()) - .username(signupUser.getUsername()) - .socialId(signupUser.getSocialInfo().getSocialId()) - .accessToken(jwt.accessToken()) - .refreshToken(jwt.refreshToken()) - .build(); + return SocialSignupResponse.of( + signupUser.getId(), + signupUser.getUsername(), + signupUser.getSocialInfo().getSocialId(), + jwt.accessToken(), + jwt.refreshToken() + ); } @Transactional @@ -70,7 +63,7 @@ public SocialLoginResponse login(SocialLoginRequest request) { loginUser.updateDeviceToken(request.deviceToken()); } - if (!request.platform().equals(loginUser.getPlatform())) { + if (!request.platform().equals(loginUser.getSocialInfo().getPlatform())) { loginUser.updatePlatform(request.platform()); } diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/UserCreator.java b/src/main/java/io/sobok/SobokSobok/auth/application/UserCreator.java new file mode 100644 index 0000000..4122c39 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/application/UserCreator.java @@ -0,0 +1,20 @@ +package io.sobok.SobokSobok.auth.application; + +import io.sobok.SobokSobok.auth.domain.Platform; +import io.sobok.SobokSobok.auth.domain.SocialInfo; +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserCreator { + + private UserRepository userRepository; + + public User create(final String username, final String socialId, final Platform platform, final String deviceToken, final String roles) { + SocialInfo socialInfo = SocialInfo.newInstance(socialId, platform); + return User.newInstance(username, socialInfo, deviceToken, roles); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java b/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java index abd369e..5815532 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java +++ b/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java @@ -58,7 +58,7 @@ public void changeUsername(Long userId, String username) { User user = UserServiceUtil.findUserById(userRepository, userId); if (duplicateNickname(username)) { - throw new ConflictException(ErrorCode.ALREADY_USING_USERNAME); + throw new ConflictException(ErrorCode.ALREADY_USING_NICKNAME); } user.changeUsername(username); diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java b/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java index dbd1021..4dc93fd 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java +++ b/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java @@ -3,6 +3,7 @@ import io.sobok.SobokSobok.auth.domain.User; import io.sobok.SobokSobok.auth.infrastructure.UserRepository; import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.ConflictException; import io.sobok.SobokSobok.exception.model.NotFoundException; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -10,6 +11,18 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class UserServiceUtil { + public static void checkAlreadySignupSocialId(UserRepository userRepository, String socialId) { + if (userRepository.existsBySocialInfoSocialId(socialId)) { + throw new ConflictException(ErrorCode.ALREADY_EXISTS_USER); + } + } + + public static void checkAlreadyUsedNickname(UserRepository userRepository, String nickname) { + if (userRepository.existsByUsername(nickname)) { + throw new ConflictException(ErrorCode.ALREADY_USING_NICKNAME); + } + } + public static User findUserById(UserRepository userRepository, Long id) { return userRepository.findById(id) .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_USER)); diff --git a/src/main/java/io/sobok/SobokSobok/auth/domain/SocialInfo.java b/src/main/java/io/sobok/SobokSobok/auth/domain/SocialInfo.java index ed383b4..8232d64 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/domain/SocialInfo.java +++ b/src/main/java/io/sobok/SobokSobok/auth/domain/SocialInfo.java @@ -5,7 +5,6 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,12 +16,24 @@ public class SocialInfo { @Column(nullable = false) private String socialId; - public void removeSocialInfo() { - this.socialId = ""; + @Column + @Enumerated(EnumType.STRING) + private Platform platform; + + public static SocialInfo newInstance(String socialId, Platform platform) { + return new SocialInfo(socialId, platform); } - @Builder - public SocialInfo(String socialId) { + private SocialInfo(String socialId, Platform platform) { this.socialId = socialId; + this.platform = platform; + } + + public void changeSocialPlatform(Platform platform) { + this.platform = platform; + } + + public void removeSocialInfo() { + this.socialId = ""; } } diff --git a/src/main/java/io/sobok/SobokSobok/auth/domain/User.java b/src/main/java/io/sobok/SobokSobok/auth/domain/User.java index 0f719a9..ef59102 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/domain/User.java +++ b/src/main/java/io/sobok/SobokSobok/auth/domain/User.java @@ -49,17 +49,15 @@ public class User extends BaseEntity implements UserDetails { @Column private String leaveReason; - @Column - @Enumerated(EnumType.STRING) - private Platform platform; + public static User newInstance(String username, SocialInfo socialInfo, String deviceToken, String roles) { + return new User(username, socialInfo, deviceToken, roles); + } - @Builder - public User(String username, SocialInfo socialInfo, String deviceToken, String roles, Platform platform) { + private User(String username, SocialInfo socialInfo, String deviceToken, String roles) { this.username = username; this.socialInfo = socialInfo; this.deviceToken = deviceToken; this.roles = roles; - this.platform = platform; } public void updateDeviceToken(String newDeviceToken) { @@ -71,7 +69,7 @@ public void changeUsername(String username) { } public void updatePlatform(Platform platform) { - this.platform = platform; + this.socialInfo.changeSocialPlatform(platform); } public void deleteUser(String leaveReason) { diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java index e8b391a..2b66807 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java @@ -4,23 +4,25 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record SocialSignupRequest( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank + @NotBlank(message = "소셜 아이디가 입력되지 않았습니다.") String socialId, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank - String username, + @NotBlank(message = "닉네임이 입력되지 않았습니다.") + @Size(min = 2, max = 10, message = "닉네임은 2글자 이상 10글자 이하만 가능합니다.") + String nickname, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank + @NotBlank(message = "디바이스 토큰이 입력되지 않았습니다.") String deviceToken, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull + @NotNull(message = "사용자의 모바일 플랫폼이 입력되지 않았습니다.") Platform platform ) { } diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupResponse.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupResponse.java index 5e4d4b0..5ad2cba 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupResponse.java +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupResponse.java @@ -15,4 +15,13 @@ public record SocialSignupResponse( String refreshToken ) { + public static SocialSignupResponse of( + final Long id, + final String username, + final String socialId, + final String accessToken, + final String refreshToken + ) { + return new SocialSignupResponse(id, username, socialId, accessToken, refreshToken); + } } diff --git a/src/main/java/io/sobok/SobokSobok/common/dto/ApiResponse.java b/src/main/java/io/sobok/SobokSobok/common/dto/ApiResponse.java index 0ad8214..bd1b912 100644 --- a/src/main/java/io/sobok/SobokSobok/common/dto/ApiResponse.java +++ b/src/main/java/io/sobok/SobokSobok/common/dto/ApiResponse.java @@ -26,4 +26,8 @@ public static ApiResponse success(SuccessCode successCode, T data) { public static ApiResponse error(ErrorCode errorCode) { return new ApiResponse<>(errorCode.getCode().value(), errorCode.getMessage(), null); } + + public static ApiResponse error(ErrorCode errorCode, String message) { + return new ApiResponse<>(errorCode.getCode().value(), message, null); + } } diff --git a/src/main/java/io/sobok/SobokSobok/exception/ControllerExceptionAdvice.java b/src/main/java/io/sobok/SobokSobok/exception/ControllerExceptionAdvice.java index f3c283c..63e4a38 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/ControllerExceptionAdvice.java +++ b/src/main/java/io/sobok/SobokSobok/exception/ControllerExceptionAdvice.java @@ -7,11 +7,16 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.HashMap; +import java.util.Map; + @RestControllerAdvice @Slf4j public class ControllerExceptionAdvice { @@ -43,6 +48,19 @@ protected ResponseEntity> handleHttpRequestMethodNotSupportedE ); } + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity> handleMethodArgumentNotValidException(final MethodArgumentNotValidException exception) { + StringBuilder sb = new StringBuilder(); + for (FieldError error : exception.getBindingResult().getFieldErrors()) { + sb.append(error.getField()).append(": ").append(error.getDefaultMessage()).append(" "); + } + + return new ResponseEntity<>( + ApiResponse.error(ErrorCode.BAD_REQUEST_EXCEPTION, sb.toString()), + HttpStatus.BAD_REQUEST + ); + } + /** * external Error */ diff --git a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java index 74d21de..e674bc6 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java @@ -22,7 +22,7 @@ public enum ErrorCode { UNREGISTERED_TOKEN(HttpStatus.NOT_FOUND, "등록되지 않은 토큰입니다."), NOT_LOGGED_IN_USER(HttpStatus.NOT_FOUND, "로그인되지 않은 사용자입니다."), ALREADY_EXISTS_USER(HttpStatus.CONFLICT, "이미 회원가입이 완료된 사용자입니다."), - ALREADY_USING_USERNAME(HttpStatus.CONFLICT, "이미 사용중인 username입니다."), + ALREADY_USING_NICKNAME(HttpStatus.CONFLICT, "이미 사용중인 닉네임입니다."), EMPTY_DEVICE_TOKEN(HttpStatus.NOT_FOUND, "디바이스 토큰이 존재하지 않습니다."), // jwt diff --git a/src/main/java/io/sobok/SobokSobok/external/firebase/FCMPushService.java b/src/main/java/io/sobok/SobokSobok/external/firebase/FCMPushService.java index 89c6224..4d1f647 100644 --- a/src/main/java/io/sobok/SobokSobok/external/firebase/FCMPushService.java +++ b/src/main/java/io/sobok/SobokSobok/external/firebase/FCMPushService.java @@ -32,13 +32,13 @@ public void sendNotificationByTokenWithFriendData(PushNotificationRequest reques private void sendNotification(PushNotificationRequest request, String friendId) { User user = UserServiceUtil.findUserById(userRepository, request.userId()); Message.Builder messageBuilder; - if (user.getPlatform().equals(Platform.ANDROID)) { + if (user.getSocialInfo().getPlatform().equals(Platform.ANDROID)) { messageBuilder = Message.builder() .setToken(user.getDeviceToken()) .putData("title", request.title()) .putData("body", request.body() == null ? "" : request.body()) .putData("type", request.type()); - } else if (user.getPlatform().equals(Platform.iOS)) { + } else if (user.getSocialInfo().getPlatform().equals(Platform.iOS)) { messageBuilder = Message.builder() .setToken(user.getDeviceToken()) .setNotification(buildNotification(request))