diff --git a/src/main/java/wanted/media/exception/ErrorCode.java b/src/main/java/wanted/media/exception/ErrorCode.java index dd34485..b709326 100644 --- a/src/main/java/wanted/media/exception/ErrorCode.java +++ b/src/main/java/wanted/media/exception/ErrorCode.java @@ -10,7 +10,12 @@ public enum ErrorCode { ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."), // 클라이언트의 입력 값에 대한 일반적인 오류 (@PathVariable, @RequestParam가 잘못되었을 때) INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "클라이언트의 입력 값을 확인해주세요."), - INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."); + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), + VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "인증코드가 일치하지 않습니다."), + VERIFICATION_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "만료된 인증코드입니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/wanted/media/exception/InvalidPasswordException.java b/src/main/java/wanted/media/exception/InvalidPasswordException.java new file mode 100644 index 0000000..3ff8264 --- /dev/null +++ b/src/main/java/wanted/media/exception/InvalidPasswordException.java @@ -0,0 +1,8 @@ +package wanted.media.exception; + +public class InvalidPasswordException extends BaseException { + public InvalidPasswordException() { + + super(ErrorCode.INVALID_PASSWORD); + } +} diff --git a/src/main/java/wanted/media/exception/UserNotFoundException.java b/src/main/java/wanted/media/exception/UserNotFoundException.java new file mode 100644 index 0000000..5e8535d --- /dev/null +++ b/src/main/java/wanted/media/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class UserNotFoundException extends BaseException { + public UserNotFoundException() { + super(ErrorCode.USER_NOT_FOUND); + } +} diff --git a/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java b/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java new file mode 100644 index 0000000..6fd18c3 --- /dev/null +++ b/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class VerificationCodeExpiredException extends BaseException { + public VerificationCodeExpiredException() { + super(ErrorCode.VERIFICATION_CODE_EXPIRED); + } +} diff --git a/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java b/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java new file mode 100644 index 0000000..d5f538d --- /dev/null +++ b/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class VerificationCodeMismatchException extends BaseException { + public VerificationCodeMismatchException() { + super(ErrorCode.VERIFICATION_CODE_MISMATCH); + } +} diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index 0ee7a8f..e2b74ce 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -5,8 +5,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import wanted.media.exception.CustomException; +import wanted.media.exception.BaseException; import wanted.media.exception.ErrorCode; +import wanted.media.exception.CustomException; import wanted.media.exception.ErrorResponse; import wanted.media.exception.NotFoundException; @@ -18,7 +19,6 @@ public ResponseEntity handleBadRequestException(BadRequestExcepti return ResponseEntity.badRequest() .body(new ErrorResponse(400, e.getMessage())); } - @ExceptionHandler(NotFoundException.class) public ResponseEntity handlePostNotFound(NotFoundException e) { ErrorCode errorCode = e.getErrorCode(); @@ -28,11 +28,16 @@ public ResponseEntity handlePostNotFound(NotFoundException e) { ); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } - @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(final CustomException e) { return ResponseEntity .status(e.getErrorCode().getStatus().value()) .body(new ErrorResponse(e.getErrorCode().getStatus().value(), e.getCustomMessage())); } + @ExceptionHandler(BaseException.class) + public ResponseEntity handleBaseException(BaseException e) { + ErrorCode errorCode = e.getErrorCode(); + return ResponseEntity.status(errorCode.getStatus()) + .body(new ErrorResponse(errorCode.getStatus().value(), errorCode.getMessage())); + } } diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 1d649c4..d524408 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -10,8 +10,11 @@ import org.springframework.web.bind.annotation.RestController; import wanted.media.user.dto.SignUpRequest; import wanted.media.user.dto.SignUpResponse; +import wanted.media.user.dto.VerifyRequest; +import wanted.media.user.dto.VerifyResponse; import wanted.media.user.dto.UserLoginRequestDto; import wanted.media.user.dto.UserLoginResponseDto; +import wanted.media.user.dto.*; import wanted.media.user.service.UserService; @RestController @@ -26,10 +29,27 @@ public ResponseEntity loginUser(@RequestBody UserLoginRequ return ResponseEntity.ok().body(responseDto); } - //회원가입 + // 회원가입 API @PostMapping("/sign-up") public ResponseEntity signUp(@Validated @RequestBody SignUpRequest request) { SignUpResponse response = userService.signUp(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + /* + * 가입승인 API + * 회원등급 (normal -> premium) + * */ + @PostMapping("/approve") + public ResponseEntity approveSignUp(@RequestBody VerifyRequest request) { + VerifyResponse response = userService.approveSignUp(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + // 인증코드 재발급 요청 API + @PostMapping("/reissue-code") + public ResponseEntity reissueCode(@RequestBody ReissueCodeRequest request) { + ReissueCodeResponse response = userService.reissueCode(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } } diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index c6167c8..2811fe4 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -19,7 +19,7 @@ public class Code { @Column(nullable = false) private Long codeId; - @OneToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; diff --git a/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java b/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java new file mode 100644 index 0000000..00b9911 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record ReissueCodeRequest(String account, String password) { +} diff --git a/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java b/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java new file mode 100644 index 0000000..ace75df --- /dev/null +++ b/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record ReissueCodeResponse(String message, String newAuthCode) { +} diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java new file mode 100644 index 0000000..c2899a4 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -0,0 +1,6 @@ +package wanted.media.user.dto; + +import wanted.media.user.domain.Grade; + +public record UserInfoDto(String account, String email, Grade grade) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/dto/VerifyRequest.java b/src/main/java/wanted/media/user/dto/VerifyRequest.java new file mode 100644 index 0000000..3d0b086 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/VerifyRequest.java @@ -0,0 +1,11 @@ +package wanted.media.user.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record VerifyRequest( + @NotBlank @Size(max = 50) String account, + @NotBlank @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") String password, + @NotBlank @Size(max = 10) String inputCode +) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/dto/VerifyResponse.java b/src/main/java/wanted/media/user/dto/VerifyResponse.java new file mode 100644 index 0000000..57164b5 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/VerifyResponse.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record VerifyResponse(String message, UserInfoDto dto) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/repository/CodeRepository.java b/src/main/java/wanted/media/user/repository/CodeRepository.java index d24c229..cede332 100644 --- a/src/main/java/wanted/media/user/repository/CodeRepository.java +++ b/src/main/java/wanted/media/user/repository/CodeRepository.java @@ -1,10 +1,23 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import wanted.media.user.domain.Code; import wanted.media.user.domain.User; +import java.util.List; +import java.util.Optional; + public interface CodeRepository extends JpaRepository { - // 사용자별 인증코드 중복확인 - boolean existsByUserAndAuthCode(User user, String newAuthCode); + // 특정 사용자 인증코드로 조회 + Optional findByUserAndAuthCode(User user, String authCode); + + //사용자가 발급받은 인증코드 삭제 + void deleteByUser(User user); + + // 사용자에 대해 모든 인증 코드 조회 + @Query("SELECT c FROM Code c WHERE c.user = :user ORDER BY c.createdTime DESC") + List findAllByUserOrderByCreatedTimeDesc(@Param("user") User user); + } diff --git a/src/main/java/wanted/media/user/repository/UserRepository.java b/src/main/java/wanted/media/user/repository/UserRepository.java index f48d108..3cff36c 100644 --- a/src/main/java/wanted/media/user/repository/UserRepository.java +++ b/src/main/java/wanted/media/user/repository/UserRepository.java @@ -1,6 +1,10 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import wanted.media.user.domain.Grade; import wanted.media.user.domain.User; import java.util.Optional; @@ -12,4 +16,9 @@ public interface UserRepository extends JpaRepository { // 사용자 이메일로 회원 조회 Optional findByEmail(String email); + + // 가입인증 회원 등급 변경 + @Modifying + @Query("UPDATE User u SET u.grade = :grade WHERE u.account = :account") + void updateUserGrade(@Param("account") String account, @Param("grade") Grade grade); } diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 49da7b8..3ab0392 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,10 +1,14 @@ package wanted.media.user.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import wanted.media.user.config.TokenProvider; +import wanted.media.exception.InvalidPasswordException; +import wanted.media.exception.UserNotFoundException; +import wanted.media.exception.VerificationCodeExpiredException; +import wanted.media.exception.VerificationCodeMismatchException; import wanted.media.user.domain.Code; import wanted.media.user.domain.Grade; import wanted.media.user.domain.Token; @@ -16,6 +20,7 @@ import java.time.LocalDateTime; import java.util.Optional; +import java.util.List; @Service @RequiredArgsConstructor @@ -29,7 +34,7 @@ public class UserService { private final UserValidator userValidator; private final GenerateCode generateCode; - @Transactional + public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); @@ -77,4 +82,57 @@ public SignUpResponse signUp(SignUpRequest request) { // 9. SignUpResponse 생성 및 반환 return new SignUpResponse("회원가입이 완료되었습니다.", userCreateDto, verificationCode); } + + //가입승인 + public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { + // 1. account로 사용자 조회 + User user = userRepository.findByAccount(verifyRequest.account()) + .orElseThrow(UserNotFoundException::new); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(verifyRequest.password(), user.getPassword())) { + throw new InvalidPasswordException(); + } + // 3. 사용자 인증코드 검증 + Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.inputCode()) + .orElseThrow(VerificationCodeMismatchException::new); + // 4. 해당 사용자의 모든 인증코드 조회 (최신순 정렬) + List userCodes = codeRepository.findAllByUserOrderByCreatedTimeDesc(user); + // 5. 해당 사용자에게 발급된 모든 인증코드와 입력된 인증코드 일치 조회 + if (!userCodes.isEmpty() && !userCodes.get(0).equals(code)) { + throw new VerificationCodeMismatchException(); + } + // 6. 인증코드 유효성 검증 (유효시간 15분) + if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) { + throw new VerificationCodeExpiredException(); + } + // 7. 인증 완료 -> 회원 등급 변경 (normal -> premium) + userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER); + // 8. 인증 완료 회원 인증코드 삭제 + codeRepository.deleteByUser(user); + return new VerifyResponse("인증이 성공적으로 완료되었습니다!", + new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade())); + } + + // 인증코드 재발급 + public ReissueCodeResponse reissueCode(ReissueCodeRequest reissueCodeRequest) { + // 1. account로 사용자 조회 + User user = userRepository.findByAccount(reissueCodeRequest.account()) + .orElseThrow(UserNotFoundException::new); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(reissueCodeRequest.password(), user.getPassword())) { + throw new InvalidPasswordException(); + } + // 3. 새로운 인증코드 발급 + String newAuthCode = generateCode.codeGenerate(); + // 4. 코드 객체 생성 + Code newCode = Code.builder() + .user(user) + .authCode(newAuthCode) + .createdTime(LocalDateTime.now()) + .build(); + // 5. 코드 db 저장 + codeRepository.save(newCode); + + return new ReissueCodeResponse("인증코드가 성공적으로 재발급되었습니다.", newAuthCode); + } }