From 80d1206b943124fe2d6304c8f5095bd85eb0dc04 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Fri, 9 Oct 2026 15:52:40 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=B3=B8=20=EA=B5=AC=EC=A1=B0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +++ .../user/dto/request/SignupRequest.java | 23 ++++++++++ .../com/select/domain/user/entity/User.java | 43 +++++++++++++++++++ .../user/repository/UserRepository.java | 10 +++++ .../domain/user/service/UserService.java | 32 ++++++++++++++ .../global/security/SecurityConfig.java | 15 +++++++ 6 files changed, 129 insertions(+) create mode 100644 src/main/java/com/select/domain/user/dto/request/SignupRequest.java create mode 100644 src/main/java/com/select/domain/user/entity/User.java create mode 100644 src/main/java/com/select/domain/user/repository/UserRepository.java create mode 100644 src/main/java/com/select/domain/user/service/UserService.java create mode 100644 src/main/java/com/select/global/security/SecurityConfig.java diff --git a/build.gradle b/build.gradle index 42467c5..72bad10 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,14 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.security:spring-security-crypto' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java new file mode 100644 index 0000000..8f97e78 --- /dev/null +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -0,0 +1,23 @@ +package com.select.domain.user.dto.request; + +import lombok.Getter; + +@Getter +public class SignupRequest { + + + private final String email; + + private final String password; + + private final String passwordConfirm; + + private final String nickname; + + public SignupRequest(String email,String password,String passwordConfirm,String nickname) { + this.email = email; + this.password = password; + this.passwordConfirm = passwordConfirm; + this.nickname = nickname; + } +} diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java new file mode 100644 index 0000000..48908bc --- /dev/null +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -0,0 +1,43 @@ +package com.select.domain.user.entity; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false,unique = true,length = 50) + private String loginId; + + @Column(nullable = false, length = 255) + private String passwordHash; + + @Column(nullable = false,length = 50) + private String nickname; + + @Column(nullable = false) + private LocalDateTime createAt; + + @Column(nullable = false) + private LocalDateTime updateAt; + + protected User() { + + } + + @Builder + public User(String loginId, String passwordHash, String nickname) { + this.loginId = loginId; + this.passwordHash = passwordHash; + this.nickname = nickname; + } +} diff --git a/src/main/java/com/select/domain/user/repository/UserRepository.java b/src/main/java/com/select/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..67182df --- /dev/null +++ b/src/main/java/com/select/domain/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.select.domain.user.repository; + +import com.select.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + boolean existsByLoginId(String loginId); + +} diff --git a/src/main/java/com/select/domain/user/service/UserService.java b/src/main/java/com/select/domain/user/service/UserService.java new file mode 100644 index 0000000..ac8d673 --- /dev/null +++ b/src/main/java/com/select/domain/user/service/UserService.java @@ -0,0 +1,32 @@ +package com.select.domain.user.service; + + +import com.select.domain.user.dto.request.SignupRequest; +import com.select.domain.user.entity.User; +import com.select.domain.user.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public UserService(UserRepository userRepository,PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + public void saveUser(SignupRequest request) { + if (userRepository.existsByLoginId(request.getEmail())) { + throw new IllegalArgumentException("이미 사용 중인 이메일입니다."); + } + User user = User + .builder() + .loginId(request.getEmail()) + .nickname(request.getNickname()) + .passwordHash(passwordEncoder.encode(request.getPassword())) + .build(); + userRepository.save(user); + } +} diff --git a/src/main/java/com/select/global/security/SecurityConfig.java b/src/main/java/com/select/global/security/SecurityConfig.java new file mode 100644 index 0000000..3283116 --- /dev/null +++ b/src/main/java/com/select/global/security/SecurityConfig.java @@ -0,0 +1,15 @@ +package com.select.global.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} From 13005d8f2ce5cc48ce9d676e95af8b88ec49c712 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Fri, 9 Oct 2026 22:11:27 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EC=99=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../user/controller/UserController.java | 32 +++++++++ .../user/dto/request/SignupRequest.java | 30 ++++---- .../user/dto/response/SignupResponse.java | 8 +++ .../com/select/domain/user/entity/User.java | 24 +++++-- .../exception/DuplicateEmailException.java | 8 +++ .../exception/PasswordMismatchException.java | 8 +++ .../domain/user/service/UserService.java | 23 ++++-- .../exception/GlobalExceptionHandler.java | 72 +++++++++++++++++++ .../com/select/global/response/ApiError.java | 9 +++ .../select/global/response/ApiResponse.java | 15 ++++ .../select/global/response/ErrorDetail.java | 7 ++ 12 files changed, 212 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/select/domain/user/controller/UserController.java create mode 100644 src/main/java/com/select/domain/user/dto/response/SignupResponse.java create mode 100644 src/main/java/com/select/domain/user/exception/DuplicateEmailException.java create mode 100644 src/main/java/com/select/domain/user/exception/PasswordMismatchException.java create mode 100644 src/main/java/com/select/global/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/select/global/response/ApiError.java create mode 100644 src/main/java/com/select/global/response/ApiResponse.java create mode 100644 src/main/java/com/select/global/response/ErrorDetail.java diff --git a/build.gradle b/build.gradle index 72bad10..70a89dd 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.security:spring-security-crypto' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -28,6 +29,8 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + runtimeOnly 'com.mysql:mysql-connector-j' } tasks.named('test') { diff --git a/src/main/java/com/select/domain/user/controller/UserController.java b/src/main/java/com/select/domain/user/controller/UserController.java new file mode 100644 index 0000000..b081553 --- /dev/null +++ b/src/main/java/com/select/domain/user/controller/UserController.java @@ -0,0 +1,32 @@ +package com.select.domain.user.controller; + +import com.select.domain.user.dto.request.SignupRequest; +import com.select.domain.user.dto.response.SignupResponse; +import com.select.domain.user.service.UserService; +import com.select.global.response.ApiResponse; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/babi/api/v1/auth") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @PostMapping("/signup") + public ApiResponse saveUser(@Valid @RequestBody SignupRequest request) { + SignupResponse response = userService.saveUser(request); + return new ApiResponse( + true, + "회원가입이 완료되었습니다.", + response + ); + } +} diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index 8f97e78..961471a 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -1,23 +1,25 @@ package com.select.domain.user.dto.request; -import lombok.Getter; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; -@Getter -public class SignupRequest { +public record SignupRequest( + @Email + @Size(max = 50) + @NotBlank + String email, - private final String email; + @NotBlank + String password, - private final String password; + @NotBlank + String passwordConfirm, - private final String passwordConfirm; + @NotBlank + @Size(max = 50) + String nickname - private final String nickname; - - public SignupRequest(String email,String password,String passwordConfirm,String nickname) { - this.email = email; - this.password = password; - this.passwordConfirm = passwordConfirm; - this.nickname = nickname; - } +) { } diff --git a/src/main/java/com/select/domain/user/dto/response/SignupResponse.java b/src/main/java/com/select/domain/user/dto/response/SignupResponse.java new file mode 100644 index 0000000..329d2c7 --- /dev/null +++ b/src/main/java/com/select/domain/user/dto/response/SignupResponse.java @@ -0,0 +1,8 @@ +package com.select.domain.user.dto.response; + +public record SignupResponse( + Long userId, + String email, + String nickname +) { +} diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java index 48908bc..910fd2a 100644 --- a/src/main/java/com/select/domain/user/entity/User.java +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -15,25 +15,37 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false,unique = true,length = 50) + @Column(name = "login_id",nullable = false,unique = true,length = 50) private String loginId; - @Column(nullable = false, length = 255) + @Column(name = "password_hash",nullable = false, length = 255) private String passwordHash; @Column(nullable = false,length = 50) private String nickname; - @Column(nullable = false) - private LocalDateTime createAt; + @Column(name = "created_at",nullable = false) + private LocalDateTime createdAt; - @Column(nullable = false) - private LocalDateTime updateAt; + @Column(name = "updated_at",nullable = false) + private LocalDateTime updatedAt; protected User() { } + @PrePersist + public void onCreated() { + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; + this.updatedAt = now; + } + + @PreUpdate + public void onUpdated() { + this.updatedAt = LocalDateTime.now(); + } + @Builder public User(String loginId, String passwordHash, String nickname) { this.loginId = loginId; diff --git a/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java b/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java new file mode 100644 index 0000000..559a75f --- /dev/null +++ b/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java @@ -0,0 +1,8 @@ +package com.select.domain.user.exception; + +public class DuplicateEmailException extends RuntimeException{ + + public DuplicateEmailException() { + super("이미 가입된 이메일입니다."); + } +} diff --git a/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java b/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java new file mode 100644 index 0000000..549fec7 --- /dev/null +++ b/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java @@ -0,0 +1,8 @@ +package com.select.domain.user.exception; + +public class PasswordMismatchException extends RuntimeException{ + + public PasswordMismatchException() { + super("비밀번호가 일치하지 않습니다."); + } +} diff --git a/src/main/java/com/select/domain/user/service/UserService.java b/src/main/java/com/select/domain/user/service/UserService.java index ac8d673..40dbc91 100644 --- a/src/main/java/com/select/domain/user/service/UserService.java +++ b/src/main/java/com/select/domain/user/service/UserService.java @@ -2,7 +2,10 @@ import com.select.domain.user.dto.request.SignupRequest; +import com.select.domain.user.dto.response.SignupResponse; import com.select.domain.user.entity.User; +import com.select.domain.user.exception.DuplicateEmailException; +import com.select.domain.user.exception.PasswordMismatchException; import com.select.domain.user.repository.UserRepository; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -17,16 +20,22 @@ public UserService(UserRepository userRepository,PasswordEncoder passwordEncoder this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } - public void saveUser(SignupRequest request) { - if (userRepository.existsByLoginId(request.getEmail())) { - throw new IllegalArgumentException("이미 사용 중인 이메일입니다."); + public SignupResponse saveUser(SignupRequest request) { + if (userRepository.existsByLoginId(request.email())) { + throw new DuplicateEmailException(); + } + if (!request.password().equals(request.passwordConfirm())) { + throw new PasswordMismatchException(); } User user = User .builder() - .loginId(request.getEmail()) - .nickname(request.getNickname()) - .passwordHash(passwordEncoder.encode(request.getPassword())) + .loginId(request.email()) + .nickname(request.nickname()) + .passwordHash(passwordEncoder.encode(request.password())) .build(); - userRepository.save(user); + User savedUser = userRepository.save(user); + return new SignupResponse(savedUser.getId(), + savedUser.getLoginId(), + savedUser.getNickname()); } } diff --git a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..cc048b5 --- /dev/null +++ b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,72 @@ +package com.select.global.exception; + +import com.select.domain.user.exception.DuplicateEmailException; +import com.select.domain.user.exception.PasswordMismatchException; +import com.select.global.response.ApiError; +import com.select.global.response.ApiResponse; +import com.select.global.response.ErrorDetail; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.List; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity> handleDuplicateEmail( + DuplicateEmailException e + ) { + ErrorDetail detail = new ErrorDetail("email",e.getMessage()); + ApiError error = new ApiError( + "DUPLICATE_EMAIL", + List.of(detail) + ); + ApiResponse response = new ApiResponse<>(false,e.getMessage(),null,error); + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(response); + } + + @ExceptionHandler(PasswordMismatchException.class) + public ResponseEntity> handlePasswordMismatch( + PasswordMismatchException e + ) { + ErrorDetail detail = new ErrorDetail("passwordConfirm",e.getMessage()); + ApiError error = new ApiError( + "PASSWORD_MISMATCH", + List.of(detail) + ); + ApiResponse response = new ApiResponse<>(false,e.getMessage(),null,error); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation( + MethodArgumentNotValidException e + ) { + List details = e.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> new ErrorDetail( + error.getField(), + error.getDefaultMessage() + )) + .toList(); + + ApiError error = new ApiError( + "VALIDATION_ERROR", + details + ); + ApiResponse response = new ApiResponse<>(false,"입력값을 확인해 주세요.",null,error); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response); + } + +} diff --git a/src/main/java/com/select/global/response/ApiError.java b/src/main/java/com/select/global/response/ApiError.java new file mode 100644 index 0000000..980837e --- /dev/null +++ b/src/main/java/com/select/global/response/ApiError.java @@ -0,0 +1,9 @@ +package com.select.global.response; + +import java.util.List; + +public record ApiError( + String code, + List details +) { +} diff --git a/src/main/java/com/select/global/response/ApiResponse.java b/src/main/java/com/select/global/response/ApiResponse.java new file mode 100644 index 0000000..248165f --- /dev/null +++ b/src/main/java/com/select/global/response/ApiResponse.java @@ -0,0 +1,15 @@ +package com.select.global.response; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public record ApiResponse( + boolean success, + String message, + T data, + @JsonInclude(JsonInclude.Include.NON_NULL) + ApiError error +) { + public ApiResponse(boolean success,String message,T data) { + this(success,message,data,null); + } +} diff --git a/src/main/java/com/select/global/response/ErrorDetail.java b/src/main/java/com/select/global/response/ErrorDetail.java new file mode 100644 index 0000000..64002bb --- /dev/null +++ b/src/main/java/com/select/global/response/ErrorDetail.java @@ -0,0 +1,7 @@ +package com.select.global.response; + +public record ErrorDetail( + String field, + String reason +) { +} From de08be59491d5b5afe4545ff1c2d5081710f7e2b Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 00:12:11 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20=EB=8F=99=EC=8B=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=A4=91=EB=B3=B5=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 6 +-- .../user/dto/request/SignupRequest.java | 15 ++----- .../user/dto/response/SignupResponse.java | 6 +-- .../com/select/domain/user/entity/User.java | 10 ++--- .../exception/DuplicateEmailException.java | 2 +- .../exception/PasswordMismatchException.java | 2 +- .../user/repository/UserRepository.java | 4 +- .../domain/user/service/UserService.java | 42 ++++++++++++++++--- .../exception/GlobalExceptionHandler.java | 10 ++--- .../select/global/response/ApiResponse.java | 6 +-- 10 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/select/domain/user/controller/UserController.java b/src/main/java/com/select/domain/user/controller/UserController.java index b081553..fe903b1 100644 --- a/src/main/java/com/select/domain/user/controller/UserController.java +++ b/src/main/java/com/select/domain/user/controller/UserController.java @@ -23,10 +23,6 @@ public UserController(UserService userService) { @PostMapping("/signup") public ApiResponse saveUser(@Valid @RequestBody SignupRequest request) { SignupResponse response = userService.saveUser(request); - return new ApiResponse( - true, - "회원가입이 완료되었습니다.", - response - ); + return new ApiResponse(true, "회원가입이 완료되었습니다.", response); } } diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index 961471a..01c93cc 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -6,20 +6,13 @@ public record SignupRequest( - @Email - @Size(max = 50) - @NotBlank - String email, + @Email @Size(max = 50) @NotBlank String email, - @NotBlank - String password, + @NotBlank String password, - @NotBlank - String passwordConfirm, + @NotBlank String passwordConfirm, - @NotBlank - @Size(max = 50) - String nickname + @NotBlank @Size(max = 50) String nickname ) { } diff --git a/src/main/java/com/select/domain/user/dto/response/SignupResponse.java b/src/main/java/com/select/domain/user/dto/response/SignupResponse.java index 329d2c7..896acbf 100644 --- a/src/main/java/com/select/domain/user/dto/response/SignupResponse.java +++ b/src/main/java/com/select/domain/user/dto/response/SignupResponse.java @@ -1,8 +1,8 @@ package com.select.domain.user.dto.response; public record SignupResponse( - Long userId, - String email, - String nickname + Long userId, + String email, + String nickname ) { } diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java index 910fd2a..57a657a 100644 --- a/src/main/java/com/select/domain/user/entity/User.java +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -15,19 +15,19 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "login_id",nullable = false,unique = true,length = 50) + @Column(name = "login_id", nullable = false, unique = true, length = 50) private String loginId; - @Column(name = "password_hash",nullable = false, length = 255) + @Column(name = "password_hash", nullable = false, length = 255) private String passwordHash; - @Column(nullable = false,length = 50) + @Column(nullable = false, length = 50) private String nickname; - @Column(name = "created_at",nullable = false) + @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; - @Column(name = "updated_at",nullable = false) + @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; protected User() { diff --git a/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java b/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java index 559a75f..84a82fc 100644 --- a/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java +++ b/src/main/java/com/select/domain/user/exception/DuplicateEmailException.java @@ -1,6 +1,6 @@ package com.select.domain.user.exception; -public class DuplicateEmailException extends RuntimeException{ +public class DuplicateEmailException extends RuntimeException { public DuplicateEmailException() { super("이미 가입된 이메일입니다."); diff --git a/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java b/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java index 549fec7..b075b79 100644 --- a/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java +++ b/src/main/java/com/select/domain/user/exception/PasswordMismatchException.java @@ -1,6 +1,6 @@ package com.select.domain.user.exception; -public class PasswordMismatchException extends RuntimeException{ +public class PasswordMismatchException extends RuntimeException { public PasswordMismatchException() { super("비밀번호가 일치하지 않습니다."); diff --git a/src/main/java/com/select/domain/user/repository/UserRepository.java b/src/main/java/com/select/domain/user/repository/UserRepository.java index 67182df..93b41f7 100644 --- a/src/main/java/com/select/domain/user/repository/UserRepository.java +++ b/src/main/java/com/select/domain/user/repository/UserRepository.java @@ -3,8 +3,8 @@ import com.select.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository { boolean existsByLoginId(String loginId); - + } diff --git a/src/main/java/com/select/domain/user/service/UserService.java b/src/main/java/com/select/domain/user/service/UserService.java index 40dbc91..bf05bd7 100644 --- a/src/main/java/com/select/domain/user/service/UserService.java +++ b/src/main/java/com/select/domain/user/service/UserService.java @@ -1,14 +1,16 @@ package com.select.domain.user.service; - import com.select.domain.user.dto.request.SignupRequest; import com.select.domain.user.dto.response.SignupResponse; import com.select.domain.user.entity.User; import com.select.domain.user.exception.DuplicateEmailException; import com.select.domain.user.exception.PasswordMismatchException; import com.select.domain.user.repository.UserRepository; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class UserService { @@ -16,10 +18,12 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - public UserService(UserRepository userRepository,PasswordEncoder passwordEncoder) { + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } + + @Transactional public SignupResponse saveUser(SignupRequest request) { if (userRepository.existsByLoginId(request.email())) { throw new DuplicateEmailException(); @@ -33,9 +37,35 @@ public SignupResponse saveUser(SignupRequest request) { .nickname(request.nickname()) .passwordHash(passwordEncoder.encode(request.password())) .build(); - User savedUser = userRepository.save(user); - return new SignupResponse(savedUser.getId(), - savedUser.getLoginId(), - savedUser.getNickname()); + try { + User savedUser = userRepository.saveAndFlush(user); + return new SignupResponse(savedUser.getId(), + savedUser.getLoginId(), + savedUser.getNickname()); + } catch (DataIntegrityViolationException e) { + if (isDuplicateEmailViolation(e)) { + throw new DuplicateEmailException(); + } + throw e; + } + } + + private boolean isDuplicateEmailViolation(DataIntegrityViolationException e) { + Throwable cause = e; + + while (cause != null) { + if (cause instanceof ConstraintViolationException violation) { + String name = violation.getConstraintName(); + + if ("uq_users_login_id".equals(name) + || "users.uq_users_login_id".equals(name)) { + return true; + } + } + + cause = cause.getCause(); + } + + return false; } } diff --git a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java index cc048b5..de54cd4 100644 --- a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java @@ -20,12 +20,12 @@ public class GlobalExceptionHandler { public ResponseEntity> handleDuplicateEmail( DuplicateEmailException e ) { - ErrorDetail detail = new ErrorDetail("email",e.getMessage()); + ErrorDetail detail = new ErrorDetail("email", e.getMessage()); ApiError error = new ApiError( "DUPLICATE_EMAIL", List.of(detail) ); - ApiResponse response = new ApiResponse<>(false,e.getMessage(),null,error); + ApiResponse response = new ApiResponse<>(false, e.getMessage(), null, error); return ResponseEntity .status(HttpStatus.CONFLICT) .body(response); @@ -35,12 +35,12 @@ public ResponseEntity> handleDuplicateEmail( public ResponseEntity> handlePasswordMismatch( PasswordMismatchException e ) { - ErrorDetail detail = new ErrorDetail("passwordConfirm",e.getMessage()); + ErrorDetail detail = new ErrorDetail("passwordConfirm", e.getMessage()); ApiError error = new ApiError( "PASSWORD_MISMATCH", List.of(detail) ); - ApiResponse response = new ApiResponse<>(false,e.getMessage(),null,error); + ApiResponse response = new ApiResponse<>(false, e.getMessage(), null, error); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(response); @@ -63,7 +63,7 @@ public ResponseEntity> handleValidation( "VALIDATION_ERROR", details ); - ApiResponse response = new ApiResponse<>(false,"입력값을 확인해 주세요.",null,error); + ApiResponse response = new ApiResponse<>(false, "입력값을 확인해 주세요.", null, error); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(response); diff --git a/src/main/java/com/select/global/response/ApiResponse.java b/src/main/java/com/select/global/response/ApiResponse.java index 248165f..13b775f 100644 --- a/src/main/java/com/select/global/response/ApiResponse.java +++ b/src/main/java/com/select/global/response/ApiResponse.java @@ -9,7 +9,7 @@ public record ApiResponse( @JsonInclude(JsonInclude.Include.NON_NULL) ApiError error ) { - public ApiResponse(boolean success,String message,T data) { - this(success,message,data,null); - } + public ApiResponse(boolean success, String message, T data) { + this(success, message, data, null); + } } From cac26613c1fb37f4064d38b2459c2c84348485d5 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 15:50:04 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix:=20=EC=84=9C=EB=B2=84=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84,=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../java/com/select/domain/package-info.java | 1 - .../user/controller/UserController.java | 2 +- .../domain/user/controller/package-info.java | 1 - .../select/domain/user/dto/package-info.java | 1 - .../user/dto/request/SignupRequest.java | 15 +++- .../domain/user/dto/request/package-info.java | 1 - .../user/dto/response/package-info.java | 1 - .../com/select/domain/user/entity/User.java | 10 ++- .../domain/user/entity/package-info.java | 1 - .../com/select/domain/user/package-info.java | 1 - .../domain/user/repository/package-info.java | 1 - .../domain/user/service/package-info.java | 1 - .../select/global/config/package-info.java | 1 - .../exception/GlobalExceptionHandler.java | 15 ++++ .../select/global/exception/package-info.java | 1 - .../java/com/select/global/package-info.java | 1 - .../select/global/response/package-info.java | 1 - .../select/global/security/package-info.java | 1 - .../domain/user/service/UserServiceTest.java | 73 +++++++++++++++++++ .../exception/GlobalExceptionHandlerTest.java | 34 +++++++++ 21 files changed, 144 insertions(+), 21 deletions(-) delete mode 100644 src/main/java/com/select/domain/package-info.java delete mode 100644 src/main/java/com/select/domain/user/controller/package-info.java delete mode 100644 src/main/java/com/select/domain/user/dto/package-info.java delete mode 100644 src/main/java/com/select/domain/user/dto/request/package-info.java delete mode 100644 src/main/java/com/select/domain/user/dto/response/package-info.java delete mode 100644 src/main/java/com/select/domain/user/entity/package-info.java delete mode 100644 src/main/java/com/select/domain/user/package-info.java delete mode 100644 src/main/java/com/select/domain/user/repository/package-info.java delete mode 100644 src/main/java/com/select/domain/user/service/package-info.java delete mode 100644 src/main/java/com/select/global/config/package-info.java delete mode 100644 src/main/java/com/select/global/exception/package-info.java delete mode 100644 src/main/java/com/select/global/package-info.java delete mode 100644 src/main/java/com/select/global/response/package-info.java delete mode 100644 src/main/java/com/select/global/security/package-info.java create mode 100644 src/test/java/com/select/domain/user/service/UserServiceTest.java create mode 100644 src/test/java/com/select/global/exception/GlobalExceptionHandlerTest.java diff --git a/.gitignore b/.gitignore index ce47d7e..9ed683f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ out/ .codegraph work/ outputs/ + +/.env diff --git a/src/main/java/com/select/domain/package-info.java b/src/main/java/com/select/domain/package-info.java deleted file mode 100644 index 12109a2..0000000 --- a/src/main/java/com/select/domain/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain; diff --git a/src/main/java/com/select/domain/user/controller/UserController.java b/src/main/java/com/select/domain/user/controller/UserController.java index fe903b1..a408ea1 100644 --- a/src/main/java/com/select/domain/user/controller/UserController.java +++ b/src/main/java/com/select/domain/user/controller/UserController.java @@ -23,6 +23,6 @@ public UserController(UserService userService) { @PostMapping("/signup") public ApiResponse saveUser(@Valid @RequestBody SignupRequest request) { SignupResponse response = userService.saveUser(request); - return new ApiResponse(true, "회원가입이 완료되었습니다.", response); + return new ApiResponse<>(true, "회원가입이 완료되었습니다.", response); } } diff --git a/src/main/java/com/select/domain/user/controller/package-info.java b/src/main/java/com/select/domain/user/controller/package-info.java deleted file mode 100644 index f38f714..0000000 --- a/src/main/java/com/select/domain/user/controller/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.controller; diff --git a/src/main/java/com/select/domain/user/dto/package-info.java b/src/main/java/com/select/domain/user/dto/package-info.java deleted file mode 100644 index 34caf91..0000000 --- a/src/main/java/com/select/domain/user/dto/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.dto; diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index 01c93cc..961471a 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -6,13 +6,20 @@ public record SignupRequest( - @Email @Size(max = 50) @NotBlank String email, + @Email + @Size(max = 50) + @NotBlank + String email, - @NotBlank String password, + @NotBlank + String password, - @NotBlank String passwordConfirm, + @NotBlank + String passwordConfirm, - @NotBlank @Size(max = 50) String nickname + @NotBlank + @Size(max = 50) + String nickname ) { } diff --git a/src/main/java/com/select/domain/user/dto/request/package-info.java b/src/main/java/com/select/domain/user/dto/request/package-info.java deleted file mode 100644 index a153424..0000000 --- a/src/main/java/com/select/domain/user/dto/request/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.dto.request; diff --git a/src/main/java/com/select/domain/user/dto/response/package-info.java b/src/main/java/com/select/domain/user/dto/response/package-info.java deleted file mode 100644 index fc83814..0000000 --- a/src/main/java/com/select/domain/user/dto/response/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.dto.response; diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java index 57a657a..20f690d 100644 --- a/src/main/java/com/select/domain/user/entity/User.java +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -1,6 +1,13 @@ package com.select.domain.user.entity; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; import lombok.Builder; import lombok.Getter; @@ -31,7 +38,6 @@ public class User { private LocalDateTime updatedAt; protected User() { - } @PrePersist diff --git a/src/main/java/com/select/domain/user/entity/package-info.java b/src/main/java/com/select/domain/user/entity/package-info.java deleted file mode 100644 index 398a834..0000000 --- a/src/main/java/com/select/domain/user/entity/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.entity; diff --git a/src/main/java/com/select/domain/user/package-info.java b/src/main/java/com/select/domain/user/package-info.java deleted file mode 100644 index 129d3fb..0000000 --- a/src/main/java/com/select/domain/user/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user; diff --git a/src/main/java/com/select/domain/user/repository/package-info.java b/src/main/java/com/select/domain/user/repository/package-info.java deleted file mode 100644 index f7974ed..0000000 --- a/src/main/java/com/select/domain/user/repository/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.repository; diff --git a/src/main/java/com/select/domain/user/service/package-info.java b/src/main/java/com/select/domain/user/service/package-info.java deleted file mode 100644 index 3b12e5e..0000000 --- a/src/main/java/com/select/domain/user/service/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.domain.user.service; diff --git a/src/main/java/com/select/global/config/package-info.java b/src/main/java/com/select/global/config/package-info.java deleted file mode 100644 index 89cb9b8..0000000 --- a/src/main/java/com/select/global/config/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.global.config; diff --git a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java index de54cd4..94d6b46 100644 --- a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java @@ -5,6 +5,8 @@ import com.select.global.response.ApiError; import com.select.global.response.ApiResponse; import com.select.global.response.ErrorDetail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -16,6 +18,8 @@ @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(DuplicateEmailException.class) public ResponseEntity> handleDuplicateEmail( DuplicateEmailException e @@ -69,4 +73,15 @@ public ResponseEntity> handleValidation( .body(response); } + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException( + Exception e + ) { + log.error("서버 내부 오류가 발생했습니다.", e); + ApiResponse response = new ApiResponse<>(false, "서버에 오류가 발생했습니다.", null, new ApiError("INTERNAL_SERVER_ERROR", List.of())); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(response); + } + } diff --git a/src/main/java/com/select/global/exception/package-info.java b/src/main/java/com/select/global/exception/package-info.java deleted file mode 100644 index 5cb527e..0000000 --- a/src/main/java/com/select/global/exception/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.global.exception; diff --git a/src/main/java/com/select/global/package-info.java b/src/main/java/com/select/global/package-info.java deleted file mode 100644 index 23fc703..0000000 --- a/src/main/java/com/select/global/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.global; diff --git a/src/main/java/com/select/global/response/package-info.java b/src/main/java/com/select/global/response/package-info.java deleted file mode 100644 index 03b0259..0000000 --- a/src/main/java/com/select/global/response/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.global.response; diff --git a/src/main/java/com/select/global/security/package-info.java b/src/main/java/com/select/global/security/package-info.java deleted file mode 100644 index a686fcd..0000000 --- a/src/main/java/com/select/global/security/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.select.global.security; diff --git a/src/test/java/com/select/domain/user/service/UserServiceTest.java b/src/test/java/com/select/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..016749a --- /dev/null +++ b/src/test/java/com/select/domain/user/service/UserServiceTest.java @@ -0,0 +1,73 @@ +package com.select.domain.user.service; + +import com.select.domain.user.dto.request.SignupRequest; +import com.select.domain.user.entity.User; +import com.select.domain.user.exception.DuplicateEmailException; +import com.select.domain.user.repository.UserRepository; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class UserServiceTest { + + private UserRepository userRepository; + private PasswordEncoder passwordEncoder; + private UserService userService; + + @BeforeEach + void setUp() { + userRepository = mock(UserRepository.class); + passwordEncoder = mock(PasswordEncoder.class); + userService = new UserService(userRepository, passwordEncoder); + } + + @ParameterizedTest + @ValueSource(strings = {"uq_users_login_id", "users.uq_users_login_id"}) + void signUpConvertsKnownLoginIdConstraintViolationToDuplicateEmail(String constraintName) { + DataIntegrityViolationException exception = constraintViolation(constraintName); + when(userRepository.existsByLoginId(any())).thenReturn(false); + when(passwordEncoder.encode(any())).thenReturn("encoded-password"); + when(userRepository.saveAndFlush(any(User.class))).thenThrow(exception); + + assertThrows(DuplicateEmailException.class, () -> userService.saveUser(signupRequest())); + } + + @Test + void signUpRethrowsOtherDataIntegrityViolation() { + DataIntegrityViolationException exception = constraintViolation("uq_other_constraint"); + when(userRepository.existsByLoginId(any())).thenReturn(false); + when(passwordEncoder.encode(any())).thenReturn("encoded-password"); + when(userRepository.saveAndFlush(any(User.class))).thenThrow(exception); + + assertSame(exception, assertThrows( + DataIntegrityViolationException.class, + () -> userService.saveUser(signupRequest()) + )); + } + + private SignupRequest signupRequest() { + return new SignupRequest("signup@example.com", "Test1234!", "Test1234!", "tester"); + } + + private DataIntegrityViolationException constraintViolation(String constraintName) { + ConstraintViolationException cause = new ConstraintViolationException( + "duplicate login id", + new SQLException(), + "insert into users", + constraintName + ); + return new DataIntegrityViolationException("data integrity violation", cause); + } +} diff --git a/src/test/java/com/select/global/exception/GlobalExceptionHandlerTest.java b/src/test/java/com/select/global/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..259ec8d --- /dev/null +++ b/src/test/java/com/select/global/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,34 @@ +package com.select.global.exception; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GlobalExceptionHandlerTest { + + @Test + void handleExceptionReturns500() { + GlobalExceptionHandler handler = new GlobalExceptionHandler(); + + var response = handler.handleException( + new RuntimeException("테스트 오류") + ); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + + var body = response.getBody(); + + assertNotNull(body); + assertEquals("INTERNAL_SERVER_ERROR", body.error().code()); + assertTrue(body.error().details().isEmpty()); + assertFalse(body.success()); + assertEquals("서버에 오류가 발생했습니다.", body.message()); + assertNull(body.data()); + assertNotNull(body.error()); + } +} From dfae759b2d353a9d92fb95e6518faa78fe8bb3bc Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 17:42:52 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B2=80=EC=A6=9D=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/request/SignupRequest.java | 8 ++ .../com/select/domain/user/entity/User.java | 19 ++- .../exception/GlobalExceptionHandler.java | 42 ++++++ .../user/controller/UserControllerTest.java | 123 ++++++++++++++++++ .../user/dto/request/SignupRequestTest.java | 48 +++++++ 5 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/select/domain/user/controller/UserControllerTest.java create mode 100644 src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index 961471a..7eeab8a 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -1,9 +1,12 @@ package com.select.domain.user.dto.request; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import java.nio.charset.StandardCharsets; + public record SignupRequest( @Email @@ -22,4 +25,9 @@ public record SignupRequest( String nickname ) { + @AssertTrue(message = "비밀번호는 UTF-8 기준 72바이트 이하여야 합니다.") + public boolean isPasswordWithin72Bytes() { + return password == null + || password.getBytes(StandardCharsets.UTF_8).length <= 72; + } } diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java index 20f690d..4cb8b9c 100644 --- a/src/main/java/com/select/domain/user/entity/User.java +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -1,13 +1,6 @@ package com.select.domain.user.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; @@ -15,14 +8,20 @@ @Entity @Getter -@Table(name = "users") +@Table( + name = "users", + uniqueConstraints = @UniqueConstraint( + name = "uq_users_login_id", + columnNames = "login_id" + ) +) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "login_id", nullable = false, unique = true, length = 50) + @Column(name = "login_id", nullable = false, length = 50) private String loginId; @Column(name = "password_hash", nullable = false, length = 255) diff --git a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java index 94d6b46..15fd6c4 100644 --- a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java @@ -9,6 +9,8 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -84,4 +86,44 @@ public ResponseEntity> handleException( .body(response); } + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleInvalidJson( + HttpMessageNotReadableException e + ) { + ApiError error = new ApiError( + "INVALID_REQUEST_BODY", + List.of() + ); + ApiResponse response = new ApiResponse<>( + false, + "요청 본문이 올바르지 않습니다.", + null, + error + ); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotAllowed( + HttpRequestMethodNotSupportedException e + ) { + ApiError error = new ApiError( + "METHOD_NOT_ALLOWED", + List.of() + ); + + ApiResponse response = new ApiResponse<>( + false, + "지원하지 않는 HTTP 메서드 입니다.", + null, + error + ); + + return ResponseEntity + .status(HttpStatus.METHOD_NOT_ALLOWED) + .headers(e.getHeaders()) + .body(response); + } } diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java new file mode 100644 index 0000000..34e7aa6 --- /dev/null +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -0,0 +1,123 @@ +package com.select.domain.user.controller; + +import com.select.domain.user.dto.response.SignupResponse; +import com.select.domain.user.exception.DuplicateEmailException; +import com.select.domain.user.service.UserService; +import com.select.global.exception.GlobalExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UserControllerTest { + + private static final String SIGNUP_URL = "/babi/api/v1/auth/signup"; + + private MockMvc mockMvc; + private UserService userService; + + @BeforeEach + void setUp() { + userService = mock(UserService.class); + mockMvc = MockMvcBuilders + .standaloneSetup(new UserController(userService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + } + + @Test + void malformedJsonReturnsBadRequestWithoutInternalDetails() throws Exception { + mockMvc.perform(post(SIGNUP_URL) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"email\":")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error.code").value("INVALID_REQUEST_BODY")) + .andExpect(jsonPath("$.error.details").isEmpty()); + + verifyNoInteractions(userService); + } + + @Test + void unsupportedMethodReturnsMethodNotAllowedWithAllowHeader() throws Exception { + mockMvc.perform(get(SIGNUP_URL)) + .andExpect(status().isMethodNotAllowed()) + .andExpect(header().string(HttpHeaders.ALLOW, "POST")) + .andExpect(jsonPath("$.error.code").value("METHOD_NOT_ALLOWED")) + .andExpect(jsonPath("$.error.details").isEmpty()); + } + + @Test + void passwordWith73Utf8BytesReturnsBadRequest() throws Exception { + String password = "a".repeat(73); + + mockMvc.perform(post(SIGNUP_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(signupJson(password))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + + verifyNoInteractions(userService); + } + + @Test + void duplicateEmailReturnsConflict() throws Exception { + doThrow(new DuplicateEmailException()).when(userService).saveUser(any()); + + mockMvc.perform(post(SIGNUP_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(signupJson("Test1234!"))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.error.code").value("DUPLICATE_EMAIL")); + } + + @Test + void internalErrorReturnsGenericResponseWithoutExceptionMessage() throws Exception { + doThrow(new IllegalStateException("internal diagnostic detail")) + .when(userService).saveUser(any()); + + mockMvc.perform(post(SIGNUP_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(signupJson("Test1234!"))) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("서버에 오류가 발생했습니다.")) + .andExpect(jsonPath("$.error.code").value("INTERNAL_SERVER_ERROR")) + .andExpect(jsonPath("$.error.details").isEmpty()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.not("internal diagnostic detail"))); + } + + @Test + void passwordWithExactly72Utf8BytesIsAccepted() throws Exception { + String password = "a".repeat(72); + when(userService.saveUser(any())) + .thenReturn(new SignupResponse(1L, "test@example.com", "tester")); + + mockMvc.perform(post(SIGNUP_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(signupJson(password))) + .andExpect(status().isOk()); + } + + private String signupJson(String password) { + return """ + { + "email": "test@example.com", + "password": "%s", + "passwordConfirm": "%s", + "nickname": "tester" + } + """.formatted(password, password); + } +} diff --git a/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java b/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java new file mode 100644 index 0000000..42bb0cc --- /dev/null +++ b/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java @@ -0,0 +1,48 @@ + +package com.select.domain.user.dto.request; + +import jakarta.validation.Validation; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SignupRequestTest { + + @Test + void passwordWithExactly72Utf8BytesShouldPassValidation() { + SignupRequest request = requestWithPassword("a".repeat(72)); + + assertTrue(request.isPasswordWithin72Bytes()); + assertTrue(validate(request).isEmpty()); + } + + @Test + void passwordWith73Utf8BytesShouldFailValidation() { + SignupRequest request = requestWithPassword("a".repeat(73)); + + assertFalse(request.isPasswordWithin72Bytes()); + assertFalse(validate(request).isEmpty()); + } + + @Test + void passwordWithMultibyteCharactersUsesUtf8ByteLength() { + SignupRequest allowed = requestWithPassword("가".repeat(24)); + SignupRequest rejected = requestWithPassword("가".repeat(25)); + + assertTrue(allowed.isPasswordWithin72Bytes()); + assertTrue(validate(allowed).isEmpty()); + assertFalse(rejected.isPasswordWithin72Bytes()); + assertFalse(validate(rejected).isEmpty()); + } + + private SignupRequest requestWithPassword(String password) { + return new SignupRequest("test@example.com", password, password, "test"); + } + + private java.util.Set> validate(SignupRequest request) { + try (var factory = Validation.buildDefaultValidatorFactory()) { + return factory.getValidator().validate(request); + } + } +} From f3ce93e21a70ea6c2fcd20f23f408e5511dd59a1 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 20:10:35 +0900 Subject: [PATCH 06/13] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/request/SignupRequest.java | 12 ++++++---- .../com/select/domain/user/entity/User.java | 12 +++++----- .../user/repository/UserRepository.java | 2 +- .../domain/user/service/UserService.java | 14 ++++------- .../exception/GlobalExceptionHandler.java | 18 +++++++++++---- .../user/controller/UserControllerTest.java | 12 ++++++++++ .../user/dto/request/SignupRequestTest.java | 23 +++++++++++++------ .../domain/user/service/UserServiceTest.java | 8 +++---- 8 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index 7eeab8a..e67142f 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -1,20 +1,22 @@ package com.select.domain.user.dto.request; -import jakarta.validation.constraints.AssertTrue; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; import java.nio.charset.StandardCharsets; public record SignupRequest( @Email - @Size(max = 50) + @Size(max = 320) @NotBlank String email, @NotBlank + @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.") + @Pattern( + regexp = "^[!-~]+$", + message = "비밀번호는 영문, 숫자, 특수문자만 입력할 수 있습니다." + ) String password, @NotBlank diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java index 4cb8b9c..059731d 100644 --- a/src/main/java/com/select/domain/user/entity/User.java +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -11,8 +11,8 @@ @Table( name = "users", uniqueConstraints = @UniqueConstraint( - name = "uq_users_login_id", - columnNames = "login_id" + name = "uq_users_email", + columnNames = "email" ) ) public class User { @@ -21,8 +21,8 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "login_id", nullable = false, length = 50) - private String loginId; + @Column(nullable = false, length = 320) + private String email; @Column(name = "password_hash", nullable = false, length = 255) private String passwordHash; @@ -52,8 +52,8 @@ public void onUpdated() { } @Builder - public User(String loginId, String passwordHash, String nickname) { - this.loginId = loginId; + public User(String email, String passwordHash, String nickname) { + this.email = email; this.passwordHash = passwordHash; this.nickname = nickname; } diff --git a/src/main/java/com/select/domain/user/repository/UserRepository.java b/src/main/java/com/select/domain/user/repository/UserRepository.java index 93b41f7..a9ccdf8 100644 --- a/src/main/java/com/select/domain/user/repository/UserRepository.java +++ b/src/main/java/com/select/domain/user/repository/UserRepository.java @@ -5,6 +5,6 @@ public interface UserRepository extends JpaRepository { - boolean existsByLoginId(String loginId); + boolean existsByEmail(String email); } diff --git a/src/main/java/com/select/domain/user/service/UserService.java b/src/main/java/com/select/domain/user/service/UserService.java index bf05bd7..b44ac2b 100644 --- a/src/main/java/com/select/domain/user/service/UserService.java +++ b/src/main/java/com/select/domain/user/service/UserService.java @@ -25,7 +25,7 @@ public UserService(UserRepository userRepository, PasswordEncoder passwordEncode @Transactional public SignupResponse saveUser(SignupRequest request) { - if (userRepository.existsByLoginId(request.email())) { + if (userRepository.existsByEmail(request.email())) { throw new DuplicateEmailException(); } if (!request.password().equals(request.passwordConfirm())) { @@ -33,14 +33,14 @@ public SignupResponse saveUser(SignupRequest request) { } User user = User .builder() - .loginId(request.email()) + .email(request.email()) .nickname(request.nickname()) .passwordHash(passwordEncoder.encode(request.password())) .build(); try { User savedUser = userRepository.saveAndFlush(user); return new SignupResponse(savedUser.getId(), - savedUser.getLoginId(), + savedUser.getEmail(), savedUser.getNickname()); } catch (DataIntegrityViolationException e) { if (isDuplicateEmailViolation(e)) { @@ -52,20 +52,16 @@ public SignupResponse saveUser(SignupRequest request) { private boolean isDuplicateEmailViolation(DataIntegrityViolationException e) { Throwable cause = e; - while (cause != null) { if (cause instanceof ConstraintViolationException violation) { String name = violation.getConstraintName(); - - if ("uq_users_login_id".equals(name) - || "users.uq_users_login_id".equals(name)) { + if ("email".equals(name) + || "users.email".equals(name)) { return true; } } - cause = cause.getCause(); } - return false; } } diff --git a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java index 15fd6c4..44bcef5 100644 --- a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.ArrayList; import java.util.List; @RestControllerAdvice @@ -56,14 +57,21 @@ public ResponseEntity> handlePasswordMismatch( public ResponseEntity> handleValidation( MethodArgumentNotValidException e ) { - List details = e.getBindingResult() - .getFieldErrors() - .stream() - .map(error -> new ErrorDetail( + List details = new ArrayList<>(); + + e.getBindingResult().getFieldErrors().forEach(error -> + details.add(new ErrorDetail( error.getField(), error.getDefaultMessage() )) - .toList(); + ); + + e.getBindingResult().getGlobalErrors().forEach(error -> + details.add(new ErrorDetail( + error.getObjectName(), + error.getDefaultMessage() + )) + ); ApiError error = new ApiError( "VALIDATION_ERROR", diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java index 34e7aa6..0f17e41 100644 --- a/src/test/java/com/select/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -110,6 +110,18 @@ void passwordWithExactly72Utf8BytesIsAccepted() throws Exception { .andExpect(status().isOk()); } + @Test + void passwordWithKoreanReturnsBadRequest() throws Exception { + mockMvc.perform(post(SIGNUP_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(signupJson("안녕1234!"))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")) + .andExpect(jsonPath("$.error.details[0].field").value("password")); + + verifyNoInteractions(userService); + } + private String signupJson(String password) { return """ { diff --git a/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java b/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java index 42bb0cc..3b8a867 100644 --- a/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java +++ b/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java @@ -27,13 +27,22 @@ void passwordWith73Utf8BytesShouldFailValidation() { @Test void passwordWithMultibyteCharactersUsesUtf8ByteLength() { - SignupRequest allowed = requestWithPassword("가".repeat(24)); - SignupRequest rejected = requestWithPassword("가".repeat(25)); - - assertTrue(allowed.isPasswordWithin72Bytes()); - assertTrue(validate(allowed).isEmpty()); - assertFalse(rejected.isPasswordWithin72Bytes()); - assertFalse(validate(rejected).isEmpty()); + SignupRequest validLength = new SignupRequest( + "test@example.com", + "가".repeat(24), + "가".repeat(24), + "tester" + ); + + SignupRequest invalidLength = new SignupRequest( + "test@example.com", + "가".repeat(25), + "가".repeat(25), + "tester" + ); + + assertTrue(validLength.isPasswordWithin72Bytes()); + assertFalse(invalidLength.isPasswordWithin72Bytes()); } private SignupRequest requestWithPassword(String password) { diff --git a/src/test/java/com/select/domain/user/service/UserServiceTest.java b/src/test/java/com/select/domain/user/service/UserServiceTest.java index 016749a..e0ae2f0 100644 --- a/src/test/java/com/select/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/select/domain/user/service/UserServiceTest.java @@ -34,10 +34,10 @@ void setUp() { } @ParameterizedTest - @ValueSource(strings = {"uq_users_login_id", "users.uq_users_login_id"}) - void signUpConvertsKnownLoginIdConstraintViolationToDuplicateEmail(String constraintName) { + @ValueSource(strings = {"email", "users.email"}) + void signUpConvertsKnownEmailConstraintViolationToDuplicateEmail(String constraintName) { DataIntegrityViolationException exception = constraintViolation(constraintName); - when(userRepository.existsByLoginId(any())).thenReturn(false); + when(userRepository.existsByEmail(any())).thenReturn(false); when(passwordEncoder.encode(any())).thenReturn("encoded-password"); when(userRepository.saveAndFlush(any(User.class))).thenThrow(exception); @@ -47,7 +47,7 @@ void signUpConvertsKnownLoginIdConstraintViolationToDuplicateEmail(String constr @Test void signUpRethrowsOtherDataIntegrityViolation() { DataIntegrityViolationException exception = constraintViolation("uq_other_constraint"); - when(userRepository.existsByLoginId(any())).thenReturn(false); + when(userRepository.existsByEmail(any())).thenReturn(false); when(passwordEncoder.encode(any())).thenReturn("encoded-password"); when(userRepository.saveAndFlush(any(User.class))).thenThrow(exception); From 98225b94fc458528a9e8b900cabb0d4b46ad2e68 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 20:20:38 +0900 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=B5=9C?= =?UTF-8?q?=EB=8C=80=20=EA=B8=B8=EC=9D=B4=2020=EC=9E=90=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/request/SignupRequest.java | 2 +- .../user/controller/UserControllerTest.java | 4 +- .../user/dto/request/SignupRequestTest.java | 47 +++++++------------ 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index e67142f..b7c3860 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -12,7 +12,7 @@ public record SignupRequest( String email, @NotBlank - @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.") + @Size(min = 8,max = 20, message = "비밀번호는 최소 8자 이상 20자 이하여야 합니다.") @Pattern( regexp = "^[!-~]+$", message = "비밀번호는 영문, 숫자, 특수문자만 입력할 수 있습니다." diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java index 0f17e41..9dea044 100644 --- a/src/test/java/com/select/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -61,7 +61,7 @@ void unsupportedMethodReturnsMethodNotAllowedWithAllowHeader() throws Exception @Test void passwordWith73Utf8BytesReturnsBadRequest() throws Exception { - String password = "a".repeat(73); + String password = "a".repeat(21); mockMvc.perform(post(SIGNUP_URL) .contentType(MediaType.APPLICATION_JSON) @@ -100,7 +100,7 @@ void internalErrorReturnsGenericResponseWithoutExceptionMessage() throws Excepti @Test void passwordWithExactly72Utf8BytesIsAccepted() throws Exception { - String password = "a".repeat(72); + String password = "a".repeat(20); when(userService.saveUser(any())) .thenReturn(new SignupResponse(1L, "test@example.com", "tester")); diff --git a/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java b/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java index 3b8a867..56bc5ee 100644 --- a/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java +++ b/src/test/java/com/select/domain/user/dto/request/SignupRequestTest.java @@ -10,48 +10,37 @@ class SignupRequestTest { @Test - void passwordWithExactly72Utf8BytesShouldPassValidation() { - SignupRequest request = requestWithPassword("a".repeat(72)); - - assertTrue(request.isPasswordWithin72Bytes()); - assertTrue(validate(request).isEmpty()); + void passwordWithExactly8CharactersShouldPassValidation() { + assertTrue(validatePassword("a".repeat(8))); } @Test - void passwordWith73Utf8BytesShouldFailValidation() { - SignupRequest request = requestWithPassword("a".repeat(73)); + void passwordWithExactly20CharactersShouldPassValidation() { + assertTrue(validatePassword("a".repeat(20))); + } - assertFalse(request.isPasswordWithin72Bytes()); - assertFalse(validate(request).isEmpty()); + @Test + void passwordWith7CharactersShouldFailValidation() { + assertFalse(validatePassword("a".repeat(7))); } @Test - void passwordWithMultibyteCharactersUsesUtf8ByteLength() { - SignupRequest validLength = new SignupRequest( - "test@example.com", - "가".repeat(24), - "가".repeat(24), - "tester" - ); + void passwordWith21CharactersShouldFailValidation() { + assertFalse(validatePassword("a".repeat(21))); + } - SignupRequest invalidLength = new SignupRequest( + private boolean validatePassword(String password) { + SignupRequest request = new SignupRequest( "test@example.com", - "가".repeat(25), - "가".repeat(25), + password, + password, "tester" ); - assertTrue(validLength.isPasswordWithin72Bytes()); - assertFalse(invalidLength.isPasswordWithin72Bytes()); - } - - private SignupRequest requestWithPassword(String password) { - return new SignupRequest("test@example.com", password, password, "test"); - } - - private java.util.Set> validate(SignupRequest request) { try (var factory = Validation.buildDefaultValidatorFactory()) { - return factory.getValidator().validate(request); + return factory.getValidator().validate(request).isEmpty(); } } } + + From 82a0264b47b99ed74fc900a3cd10fc7dfdd71701 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 20:58:34 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20UNIQ?= =?UTF-8?q?UE=20=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=EB=AA=85=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/select/domain/user/entity/User.java | 2 +- .../com/select/domain/user/controller/UserControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/select/domain/user/entity/User.java b/src/main/java/com/select/domain/user/entity/User.java index 059731d..a7fff3d 100644 --- a/src/main/java/com/select/domain/user/entity/User.java +++ b/src/main/java/com/select/domain/user/entity/User.java @@ -11,7 +11,7 @@ @Table( name = "users", uniqueConstraints = @UniqueConstraint( - name = "uq_users_email", + name = "email", columnNames = "email" ) ) diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java index 9dea044..6661dc6 100644 --- a/src/test/java/com/select/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -99,7 +99,7 @@ void internalErrorReturnsGenericResponseWithoutExceptionMessage() throws Excepti } @Test - void passwordWithExactly72Utf8BytesIsAccepted() throws Exception { + void passwordWith21CharactersReturnsBadRequest() throws Exception { String password = "a".repeat(20); when(userService.saveUser(any())) .thenReturn(new SignupResponse(1L, "test@example.com", "tester")); From 5c52d1a1991ea61b95d861e53b384be9c08dd278 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sat, 10 Oct 2026 21:04:47 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/select/domain/user/controller/UserControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java index 6661dc6..aa4cf6a 100644 --- a/src/test/java/com/select/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -60,7 +60,7 @@ void unsupportedMethodReturnsMethodNotAllowedWithAllowHeader() throws Exception } @Test - void passwordWith73Utf8BytesReturnsBadRequest() throws Exception { + void passwordWith21CharactersReturnsBadRequest() throws Exception { String password = "a".repeat(21); mockMvc.perform(post(SIGNUP_URL) @@ -99,7 +99,7 @@ void internalErrorReturnsGenericResponseWithoutExceptionMessage() throws Excepti } @Test - void passwordWith21CharactersReturnsBadRequest() throws Exception { + void passwordWithExactly20CharactersIsAccepted() throws Exception { String password = "a".repeat(20); when(userService.saveUser(any())) .thenReturn(new SignupResponse(1L, "test@example.com", "tester")); From 713095dc309b2b823ab4ad4f6d2b37b4e2b4b1d2 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Sun, 11 Oct 2026 20:48:14 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B0=8F=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=98=A4=EB=A5=98=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 2 +- .../domain/user/service/UserService.java | 18 ++-- .../select/global/exception/ErrorCode.java | 24 ++++++ .../exception/GlobalExceptionHandler.java | 84 +++++++------------ .../user/controller/UserControllerTest.java | 6 +- .../domain/user/service/UserServiceTest.java | 4 +- 6 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 src/main/java/com/select/global/exception/ErrorCode.java diff --git a/src/main/java/com/select/domain/user/controller/UserController.java b/src/main/java/com/select/domain/user/controller/UserController.java index a408ea1..f803c1b 100644 --- a/src/main/java/com/select/domain/user/controller/UserController.java +++ b/src/main/java/com/select/domain/user/controller/UserController.java @@ -22,7 +22,7 @@ public UserController(UserService userService) { @PostMapping("/signup") public ApiResponse saveUser(@Valid @RequestBody SignupRequest request) { - SignupResponse response = userService.saveUser(request); + SignupResponse response = userService.signup(request); return new ApiResponse<>(true, "회원가입이 완료되었습니다.", response); } } diff --git a/src/main/java/com/select/domain/user/service/UserService.java b/src/main/java/com/select/domain/user/service/UserService.java index b44ac2b..0397b6e 100644 --- a/src/main/java/com/select/domain/user/service/UserService.java +++ b/src/main/java/com/select/domain/user/service/UserService.java @@ -24,13 +24,8 @@ public UserService(UserRepository userRepository, PasswordEncoder passwordEncode } @Transactional - public SignupResponse saveUser(SignupRequest request) { - if (userRepository.existsByEmail(request.email())) { - throw new DuplicateEmailException(); - } - if (!request.password().equals(request.passwordConfirm())) { - throw new PasswordMismatchException(); - } + public SignupResponse signup(SignupRequest request) { + validateSignup(request); User user = User .builder() .email(request.email()) @@ -49,6 +44,15 @@ public SignupResponse saveUser(SignupRequest request) { throw e; } } + private void validateSignup(SignupRequest request) { + if (userRepository.existsByEmail(request.email())) { + throw new DuplicateEmailException(); + } + + if (!request.password().equals(request.passwordConfirm())) { + throw new PasswordMismatchException(); + } + } private boolean isDuplicateEmailViolation(DataIntegrityViolationException e) { Throwable cause = e; diff --git a/src/main/java/com/select/global/exception/ErrorCode.java b/src/main/java/com/select/global/exception/ErrorCode.java new file mode 100644 index 0000000..1f9bf7d --- /dev/null +++ b/src/main/java/com/select/global/exception/ErrorCode.java @@ -0,0 +1,24 @@ +package com.select.global.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 가입된 이메일입니다."), + PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), + VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "입력값을 확인해 주세요."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 오류가 발생했습니다."), + INVALID_REQUEST_BODY(HttpStatus.BAD_REQUEST, "요청 본문이 올바르지 않습니다."), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메서드 입니다."); + + private final HttpStatus httpStatus; + private final String message; + + ErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + +} diff --git a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java index 44bcef5..4a00613 100644 --- a/src/main/java/com/select/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/select/global/exception/GlobalExceptionHandler.java @@ -7,7 +7,6 @@ import com.select.global.response.ErrorDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -27,30 +26,20 @@ public class GlobalExceptionHandler { public ResponseEntity> handleDuplicateEmail( DuplicateEmailException e ) { - ErrorDetail detail = new ErrorDetail("email", e.getMessage()); - ApiError error = new ApiError( - "DUPLICATE_EMAIL", - List.of(detail) + return createErrorResponse( + ErrorCode.DUPLICATE_EMAIL, + List.of(new ErrorDetail("email", ErrorCode.DUPLICATE_EMAIL.getMessage())) ); - ApiResponse response = new ApiResponse<>(false, e.getMessage(), null, error); - return ResponseEntity - .status(HttpStatus.CONFLICT) - .body(response); } @ExceptionHandler(PasswordMismatchException.class) public ResponseEntity> handlePasswordMismatch( PasswordMismatchException e ) { - ErrorDetail detail = new ErrorDetail("passwordConfirm", e.getMessage()); - ApiError error = new ApiError( - "PASSWORD_MISMATCH", - List.of(detail) + return createErrorResponse( + ErrorCode.PASSWORD_MISMATCH, + List.of(new ErrorDetail("passwordConfirm", ErrorCode.PASSWORD_MISMATCH.getMessage())) ); - ApiResponse response = new ApiResponse<>(false, e.getMessage(), null, error); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(response); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -73,14 +62,7 @@ public ResponseEntity> handleValidation( )) ); - ApiError error = new ApiError( - "VALIDATION_ERROR", - details - ); - ApiResponse response = new ApiResponse<>(false, "입력값을 확인해 주세요.", null, error); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(response); + return createErrorResponse(ErrorCode.VALIDATION_ERROR, details); } @ExceptionHandler(Exception.class) @@ -88,50 +70,40 @@ public ResponseEntity> handleException( Exception e ) { log.error("서버 내부 오류가 발생했습니다.", e); - ApiResponse response = new ApiResponse<>(false, "서버에 오류가 발생했습니다.", null, new ApiError("INTERNAL_SERVER_ERROR", List.of())); - return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(response); + return createErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, List.of()); } @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleInvalidJson( HttpMessageNotReadableException e ) { - ApiError error = new ApiError( - "INVALID_REQUEST_BODY", - List.of() - ); - ApiResponse response = new ApiResponse<>( - false, - "요청 본문이 올바르지 않습니다.", - null, - error - ); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(response); + return createErrorResponse(ErrorCode.INVALID_REQUEST_BODY, List.of()); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity> handleMethodNotAllowed( HttpRequestMethodNotSupportedException e ) { - ApiError error = new ApiError( - "METHOD_NOT_ALLOWED", - List.of() - ); - - ApiResponse response = new ApiResponse<>( - false, - "지원하지 않는 HTTP 메서드 입니다.", - null, - error - ); - return ResponseEntity - .status(HttpStatus.METHOD_NOT_ALLOWED) + .status(ErrorCode.METHOD_NOT_ALLOWED.getHttpStatus()) .headers(e.getHeaders()) - .body(response); + .body(createErrorResponseBody(ErrorCode.METHOD_NOT_ALLOWED, List.of())); + } + + private ResponseEntity> createErrorResponse( + ErrorCode errorCode, + List details + ) { + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(createErrorResponseBody(errorCode, details)); + } + + private ApiResponse createErrorResponseBody( + ErrorCode errorCode, + List details + ) { + ApiError error = new ApiError(errorCode.name(), details); + return new ApiResponse<>(false, errorCode.getMessage(), null, error); } } diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java index aa4cf6a..3772a71 100644 --- a/src/test/java/com/select/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -74,7 +74,7 @@ void passwordWith21CharactersReturnsBadRequest() throws Exception { @Test void duplicateEmailReturnsConflict() throws Exception { - doThrow(new DuplicateEmailException()).when(userService).saveUser(any()); + doThrow(new DuplicateEmailException()).when(userService).signup(any()); mockMvc.perform(post(SIGNUP_URL) .contentType(MediaType.APPLICATION_JSON) @@ -86,7 +86,7 @@ void duplicateEmailReturnsConflict() throws Exception { @Test void internalErrorReturnsGenericResponseWithoutExceptionMessage() throws Exception { doThrow(new IllegalStateException("internal diagnostic detail")) - .when(userService).saveUser(any()); + .when(userService).signup(any()); mockMvc.perform(post(SIGNUP_URL) .contentType(MediaType.APPLICATION_JSON) @@ -101,7 +101,7 @@ void internalErrorReturnsGenericResponseWithoutExceptionMessage() throws Excepti @Test void passwordWithExactly20CharactersIsAccepted() throws Exception { String password = "a".repeat(20); - when(userService.saveUser(any())) + when(userService.signup(any())) .thenReturn(new SignupResponse(1L, "test@example.com", "tester")); mockMvc.perform(post(SIGNUP_URL) diff --git a/src/test/java/com/select/domain/user/service/UserServiceTest.java b/src/test/java/com/select/domain/user/service/UserServiceTest.java index e0ae2f0..249cc25 100644 --- a/src/test/java/com/select/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/select/domain/user/service/UserServiceTest.java @@ -41,7 +41,7 @@ void signUpConvertsKnownEmailConstraintViolationToDuplicateEmail(String constrai when(passwordEncoder.encode(any())).thenReturn("encoded-password"); when(userRepository.saveAndFlush(any(User.class))).thenThrow(exception); - assertThrows(DuplicateEmailException.class, () -> userService.saveUser(signupRequest())); + assertThrows(DuplicateEmailException.class, () -> userService.signup(signupRequest())); } @Test @@ -53,7 +53,7 @@ void signUpRethrowsOtherDataIntegrityViolation() { assertSame(exception, assertThrows( DataIntegrityViolationException.class, - () -> userService.saveUser(signupRequest()) + () -> userService.signup(signupRequest()) )); } From de4e8e9254df75b2c88a24cf55b53bac467e3381 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Mon, 12 Oct 2026 12:08:46 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EA=B2=80=EC=A6=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/select/domain/user/dto/request/SignupRequest.java | 7 ------- .../select/domain/user/controller/UserControllerTest.java | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java index b7c3860..4707a4b 100644 --- a/src/main/java/com/select/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/com/select/domain/user/dto/request/SignupRequest.java @@ -2,8 +2,6 @@ import jakarta.validation.constraints.*; -import java.nio.charset.StandardCharsets; - public record SignupRequest( @Email @@ -27,9 +25,4 @@ public record SignupRequest( String nickname ) { - @AssertTrue(message = "비밀번호는 UTF-8 기준 72바이트 이하여야 합니다.") - public boolean isPasswordWithin72Bytes() { - return password == null - || password.getBytes(StandardCharsets.UTF_8).length <= 72; - } } diff --git a/src/test/java/com/select/domain/user/controller/UserControllerTest.java b/src/test/java/com/select/domain/user/controller/UserControllerTest.java index 3772a71..7c19f81 100644 --- a/src/test/java/com/select/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/select/domain/user/controller/UserControllerTest.java @@ -114,7 +114,7 @@ void passwordWithExactly20CharactersIsAccepted() throws Exception { void passwordWithKoreanReturnsBadRequest() throws Exception { mockMvc.perform(post(SIGNUP_URL) .contentType(MediaType.APPLICATION_JSON) - .content(signupJson("안녕1234!"))) + .content(signupJson("안녕12345!"))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")) .andExpect(jsonPath("$.error.details[0].field").value("password")); From ce136a8a0fca40f00106d04009827e2da0123bf5 Mon Sep 17 00:00:00 2001 From: ryuyejun Date: Mon, 12 Oct 2026 14:15:39 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/select/domain/user/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/select/domain/user/controller/UserController.java b/src/main/java/com/select/domain/user/controller/UserController.java index f803c1b..1262c73 100644 --- a/src/main/java/com/select/domain/user/controller/UserController.java +++ b/src/main/java/com/select/domain/user/controller/UserController.java @@ -21,7 +21,7 @@ public UserController(UserService userService) { } @PostMapping("/signup") - public ApiResponse saveUser(@Valid @RequestBody SignupRequest request) { + public ApiResponse signup(@Valid @RequestBody SignupRequest request) { SignupResponse response = userService.signup(request); return new ApiResponse<>(true, "회원가입이 완료되었습니다.", response); } From 38b3770625332b75142f038e8bf593b09f145b38 Mon Sep 17 00:00:00 2001 From: Ha HeonHwi <2010haheon@gmail.com> Date: Tue, 13 Oct 2026 19:40:27 +0900 Subject: [PATCH 13/13] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GithubTest/Example.md | 2 -- GithubTest/ryuyejun.md | 2 -- GithubTest/songeunyong.md | 2 -- build.gradle | 1 - 4 files changed, 7 deletions(-) delete mode 100644 GithubTest/Example.md delete mode 100644 GithubTest/ryuyejun.md delete mode 100644 GithubTest/songeunyong.md diff --git a/GithubTest/Example.md b/GithubTest/Example.md deleted file mode 100644 index 45a6dc2..0000000 --- a/GithubTest/Example.md +++ /dev/null @@ -1,2 +0,0 @@ -# Myname = "이름" -> 테스트 \ No newline at end of file diff --git a/GithubTest/ryuyejun.md b/GithubTest/ryuyejun.md deleted file mode 100644 index 8102390..0000000 --- a/GithubTest/ryuyejun.md +++ /dev/null @@ -1,2 +0,0 @@ -# Myname = "류예준" -> 테스트ㅎ \ No newline at end of file diff --git a/GithubTest/songeunyong.md b/GithubTest/songeunyong.md deleted file mode 100644 index 493676a..0000000 --- a/GithubTest/songeunyong.md +++ /dev/null @@ -1,2 +0,0 @@ -# Myname = "송은용" -> 테스트 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 70a89dd..7cb55c1 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,6 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - runtimeOnly 'com.mysql:mysql-connector-j' }