diff --git a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java index bc03ca98a..57d6f4769 100644 --- a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.jsonwebtoken.JwtException; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -13,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import static com.example.solidconnection.common.exception.ErrorCode.DATA_INTEGRITY_VIOLATION; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_INPUT; import static com.example.solidconnection.common.exception.ErrorCode.JSON_PARSING_FAILED; import static com.example.solidconnection.common.exception.ErrorCode.JWT_EXCEPTION; @@ -56,6 +58,15 @@ public ResponseEntity handleValidationExceptions(MethodArgumentNo .body(errorResponse); } + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { + log.error("데이터 무결성 제약조건 위반 예외 발생 : {}", ex.getMessage()); + ErrorResponse errorResponse = new ErrorResponse(DATA_INTEGRITY_VIOLATION, "데이터 무결성 제약조건 위반 예외 발생"); + return ResponseEntity + .status(DATA_INTEGRITY_VIOLATION.getCode()) + .body(errorResponse); + } + @ExceptionHandler(JwtException.class) public ResponseEntity handleJwtException(JwtException ex) { String errorMessage = ex.getMessage(); diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 6e932a159..00e600201 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -96,6 +96,9 @@ public enum ErrorCode { USER_DO_NOT_HAVE_GPA(HttpStatus.BAD_REQUEST.value(), "해당 유저의 학점을 찾을 수 없음"), REJECTED_REASON_REQUIRED(HttpStatus.BAD_REQUEST.value(), "거절 사유가 필요합니다."), + // database + DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."), + // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java index ff36d7baa..772da0d32 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -1,5 +1,6 @@ package com.example.solidconnection.siteuser.controller; + import com.example.solidconnection.common.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index bcfb1f9ac..98c18b56a 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -35,6 +35,10 @@ @UniqueConstraint( name = "uk_site_user_email_auth_type", columnNames = {"email", "auth_type"} + ), + @UniqueConstraint( + name = "uk_site_user_nickname", + columnNames = {"nickname"} ) }) public class SiteUser { diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index e0617f046..51cb410f6 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -1,5 +1,6 @@ package com.example.solidconnection.siteuser.repository; + import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java index 2c84f0518..f5b463c0a 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -21,6 +21,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; import static com.example.solidconnection.common.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; @RequiredArgsConstructor @Service @@ -47,27 +48,23 @@ public MyPageResponse getMyPageInfo(SiteUser siteUser) { * */ @Transactional public void updateMyPageInfo(SiteUser siteUser, MultipartFile imageFile, String nickname) { + SiteUser user = siteUserRepository.findById(siteUser.getId()) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + if (nickname != null) { + validateNicknameNotChangedRecently(user.getNicknameModifiedAt()); validateNicknameUnique(nickname); - validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); - siteUser.setNickname(nickname); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); + user.setNickname(nickname); + user.setNicknameModifiedAt(LocalDateTime.now()); } if (imageFile != null && !imageFile.isEmpty()) { UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); - if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { - s3Service.deleteExProfile(siteUser); + if (!isDefaultProfileImage(user.getProfileImageUrl())) { + s3Service.deleteExProfile(user); } String profileImageUrl = uploadedFile.fileUrl(); - siteUser.setProfileImageUrl(profileImageUrl); - } - siteUserRepository.save(siteUser); - } - - private void validateNicknameUnique(String nickname) { - if (siteUserRepository.existsByNickname(nickname)) { - throw new CustomException(NICKNAME_ALREADY_EXISTED); + user.setProfileImageUrl(profileImageUrl); } } @@ -82,6 +79,12 @@ private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { } } + private void validateNicknameUnique(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + private boolean isDefaultProfileImage(String profileImageUrl) { String prefix = "profile/"; return profileImageUrl == null || !profileImageUrl.startsWith(prefix); diff --git a/src/main/resources/db/migration/V14__set_unique_constraint_to_nickname.sql b/src/main/resources/db/migration/V14__set_unique_constraint_to_nickname.sql new file mode 100644 index 000000000..75d290f7b --- /dev/null +++ b/src/main/resources/db/migration/V14__set_unique_constraint_to_nickname.sql @@ -0,0 +1,3 @@ +ALTER TABLE site_user +ADD CONSTRAINT uk_site_user_nickname +UNIQUE (nickname); diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index 188f717ca..472cb3aca 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -21,7 +21,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmailAndNickname; import static org.junit.jupiter.api.Assertions.assertEquals; @TestContainerSpringBootTest @@ -92,7 +92,8 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { String email = "email" + i; - SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmail(email)); + String nickname = "nickname" + i; + SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmailAndNickname(email, nickname)); executorService.submit(() -> { try { postLikeService.likePost(tmpSiteUser, post.getId()); diff --git a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java index 4c90e58dc..5187877d2 100644 --- a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java +++ b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java @@ -6,10 +6,10 @@ public class DynamicFixture { // todo: test fixture 개선 작업 이후, 이 클래스의 사용이 대체되면 삭제 필요 - public static SiteUser createSiteUserByEmail(String email) { + public static SiteUser createSiteUserByEmailAndNickname(String email, String nickname) { return new SiteUser( email, - "nickname", + nickname, "profileImage", PreparationStatus.CONSIDERING, Role.MENTEE diff --git a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java index 41806d6cf..b8c51c148 100644 --- a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java @@ -24,8 +24,8 @@ class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다 @Test void 이메일과_인증_유형이_동일한_사용자를_저장하면_예외_응답을_반환한다() { // given - SiteUser user1 = createSiteUser("email", AuthType.KAKAO); - SiteUser user2 = createSiteUser("email", AuthType.KAKAO); + SiteUser user1 = createSiteUser("email", "nickname1", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", "nickname2", AuthType.KAKAO); siteUserRepository.save(user1); // when, then @@ -36,8 +36,8 @@ class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다 @Test void 이메일이_같더라도_인증_유형이_다른_사용자는_정상_저장한다() { // given - SiteUser user1 = createSiteUser("email", AuthType.KAKAO); - SiteUser user2 = createSiteUser("email", AuthType.APPLE); + SiteUser user1 = createSiteUser("email", "nickname1", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", "nickname2", AuthType.APPLE); siteUserRepository.save(user1); // when, then @@ -46,10 +46,42 @@ class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다 } } - private SiteUser createSiteUser(String email, AuthType authType) { + @Nested + class 닉네임은_중복될_수_없다 { + + @Test + void 중복된_닉네임으로_사용자를_저장하면_예외_응답을_반환한다() { + // given + SiteUser user1 = createSiteUser("email1", "nickname", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email2", "nickname", AuthType.KAKAO); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.saveAndFlush(user2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() { + // given + SiteUser user1 = createSiteUser("email1", "nickname1", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email2", "nickname2", AuthType.KAKAO); + siteUserRepository.save(user1); + siteUserRepository.save(user2); + + // when + user2.setNickname("nickname1"); + + // then + assertThatCode(() -> siteUserRepository.saveAndFlush(user2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + } + + private SiteUser createSiteUser(String email, String nickname, AuthType authType) { return new SiteUser( email, - "nickname", + nickname, "profileImageUrl", PreparationStatus.CONSIDERING, Role.MENTEE, diff --git a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java index 1168a0eeb..a86d80883 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java @@ -29,11 +29,11 @@ import java.util.List; import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; -import static com.example.solidconnection.common.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.siteuser.service.MyPageService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; import static com.example.solidconnection.siteuser.service.MyPageService.NICKNAME_LAST_CHANGE_DATE_FORMAT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.BDDMockito.any; import static org.mockito.BDDMockito.eq; import static org.mockito.BDDMockito.given; @@ -118,7 +118,8 @@ class 프로필_이미지_수정_테스트 { myPageService.updateMyPageInfo(user, imageFile, "newNickname"); // then - assertThat(user.getProfileImageUrl()).isEqualTo(expectedUrl); + SiteUser updatedUser = siteUserRepository.findById(user.getId()).get(); + assertThat(updatedUser.getProfileImageUrl()).isEqualTo(expectedUrl); } @Test @@ -147,7 +148,8 @@ class 프로필_이미지_수정_테스트 { myPageService.updateMyPageInfo(커스텀_프로필_사용자, imageFile, "newNickname"); // then - then(s3Service).should().deleteExProfile(커스텀_프로필_사용자); + then(s3Service).should().deleteExProfile(argThat(user -> + user.getId().equals(커스텀_프로필_사용자.getId()))); } } @@ -175,23 +177,13 @@ void setUp() { assertThat(updatedUser.getNickname()).isEqualTo(newNickname); } - @Test - void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() { - // given - SiteUser existingUser = siteUserFixture.사용자(1, "existing nickname"); - - // when & then - assertThatCode(() -> myPageService.updateMyPageInfo(user, null, existingUser.getNickname())) - .isInstanceOf(CustomException.class) - .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); - } - @Test void 최소_대기기간이_지나지_않은_상태에서_변경하면_예외_응답을_반환한다() { // given MockMultipartFile imageFile = createValidImageFile(); LocalDateTime modifiedAt = LocalDateTime.now().minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES - 1); user.setNicknameModifiedAt(modifiedAt); + siteUserRepository.save(user); // when & then assertThatCode(() -> myPageService.updateMyPageInfo(user, imageFile, "nickname12"))