From 8d69622b542df24e26c4ee08adabf9a217add04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:44:05 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=20=EC=8B=9C=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=ED=9A=9F=EC=88=98=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80=20(#239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 지원서 제출 시 응답에 지원 횟수 추가 * fix: 지원서 제출 최대 횟수 3번으로 수정 * refactor: 불필요한 조건문 제거 * fix: V8 -> V10으로 변경 * style: 개행 추가 --- .../controller/ApplicationController.java | 4 +- .../application/domain/Application.java | 6 +-- .../dto/ApplicationSubmissionResponse.java | 8 +++- .../service/ApplicationSubmissionService.java | 41 ++++++++----------- ...V10__change_update_count_default_value.sql | 2 + .../ApplicationSubmissionServiceTest.java | 9 ++-- 6 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 src/main/resources/db/migration/V10__change_update_count_default_value.sql diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 892597e90..801cbf10c 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -32,10 +32,10 @@ public ResponseEntity apply( @AuthorizedUser SiteUser siteUser, @Valid @RequestBody ApplyRequest applyRequest ) { - boolean result = applicationSubmissionService.apply(siteUser, applyRequest); + ApplicationSubmissionResponse applicationSubmissionResponse = applicationSubmissionService.apply(siteUser, applyRequest); return ResponseEntity .status(HttpStatus.OK) - .body(new ApplicationSubmissionResponse(result)); + .body(applicationSubmissionResponse); } @GetMapping diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index 61dc5159e..6caa75331 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -46,7 +46,7 @@ public class Application { @Column(length = 100) private String nicknameForApply; - @Column(columnDefinition = "int not null default 0") + @Column(columnDefinition = "int not null default 1") private Integer updateCount; @Column(length = 50, nullable = false) @@ -76,7 +76,7 @@ public Application( this.gpa = gpa; this.languageTest = languageTest; this.term = term; - this.updateCount = 0; + this.updateCount = 1; this.verifyStatus = PENDING; } @@ -115,7 +115,7 @@ public Application( this.gpa = gpa; this.languageTest = languageTest; this.term = term; - this.updateCount = 0; + this.updateCount = 1; this.firstChoiceUniversity = firstChoiceUniversity; this.secondChoiceUniversity = secondChoiceUniversity; this.thirdChoiceUniversity = thirdChoiceUniversity; diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java index 4f353733b..e90994c37 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java @@ -1,5 +1,11 @@ package com.example.solidconnection.application.dto; +import com.example.solidconnection.application.domain.Application; + public record ApplicationSubmissionResponse( - boolean isSuccess) { + int applyCount +) { + public static ApplicationSubmissionResponse from(Application application) { + return new ApplicationSubmissionResponse(application.getUpdateCount()); + } } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index dfc455146..432e93aff 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.application.service; import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; @@ -49,15 +50,10 @@ public class ApplicationSubmissionService { key = {"applications:all"}, cacheManager = "customCacheManager" ) - public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { + public ApplicationSubmissionResponse apply(SiteUser siteUser, ApplyRequest applyRequest) { UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); - - Long gpaScoreId = applyRequest.gpaScoreId(); - Long languageTestScoreId = applyRequest.languageTestScoreId(); - GpaScore gpaScore = getValidGpaScore(siteUser, gpaScoreId); - LanguageTestScore languageTestScore = getValidLanguageTestScore(siteUser, languageTestScoreId); - - Optional application = applicationRepository.findBySiteUserAndTerm(siteUser, term); + GpaScore gpaScore = getValidGpaScore(siteUser, applyRequest.gpaScoreId()); + LanguageTestScore languageTestScore = getValidLanguageTestScore(siteUser, applyRequest.languageTestScoreId()); UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); @@ -68,22 +64,19 @@ public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { .map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term)) .orElse(null); - if (application.isEmpty()) { - Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(), - term, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); - newApplication.setVerifyStatus(VerifyStatus.APPROVED); - applicationRepository.save(newApplication); - } else { - Application before = application.get(); - validateUpdateLimitNotExceed(before); - before.setIsDeleteTrue(); // 기존 이력 soft delete 수행한다. - - Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(), - term, before.getUpdateCount() + 1, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); - newApplication.setVerifyStatus(VerifyStatus.APPROVED); - applicationRepository.save(newApplication); - } - return true; + Optional existingApplication = applicationRepository.findBySiteUserAndTerm(siteUser, term); + int updateCount = existingApplication + .map(application -> { + validateUpdateLimitNotExceed(application); + application.setIsDeleteTrue(); + return application.getUpdateCount() + 1; + }) + .orElse(1); + Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(), + term, updateCount, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); + newApplication.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.save(newApplication); + return ApplicationSubmissionResponse.from(newApplication); } private GpaScore getValidGpaScore(SiteUser siteUser, Long gpaScoreId) { diff --git a/src/main/resources/db/migration/V10__change_update_count_default_value.sql b/src/main/resources/db/migration/V10__change_update_count_default_value.sql new file mode 100644 index 000000000..20327b96e --- /dev/null +++ b/src/main/resources/db/migration/V10__change_update_count_default_value.sql @@ -0,0 +1,2 @@ +ALTER TABLE application + ALTER COLUMN update_count SET DEFAULT 1; diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index ffd3818ce..f4f442840 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -3,6 +3,7 @@ import com.example.solidconnection.application.domain.Application; import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; @@ -21,7 +22,6 @@ import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; -import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS; import static org.assertj.core.api.Assertions.assertThat; @@ -56,17 +56,16 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); // when - boolean result = applicationSubmissionService.apply(테스트유저_1, request); + ApplicationSubmissionResponse response = applicationSubmissionService.apply(테스트유저_1, request); // then Application savedApplication = applicationRepository.findBySiteUserAndTerm(테스트유저_1, term).orElseThrow(); assertAll( - () -> assertThat(result).isTrue(), + () -> assertThat(response.applyCount()).isEqualTo(savedApplication.getUpdateCount()), () -> assertThat(savedApplication.getGpa()).isEqualTo(gpaScore.getGpa()), () -> assertThat(savedApplication.getLanguageTest()).isEqualTo(languageTestScore.getLanguageTest()), () -> assertThat(savedApplication.getVerifyStatus()).isEqualTo(VerifyStatus.APPROVED), () -> assertThat(savedApplication.getNicknameForApply()).isNotNull(), - () -> assertThat(savedApplication.getUpdateCount()).isZero(), () -> assertThat(savedApplication.getTerm()).isEqualTo(term), () -> assertThat(savedApplication.isDelete()).isFalse(), () -> assertThat(savedApplication.getFirstChoiceUniversity().getId()).isEqualTo(괌대학_A_지원_정보.getId()), @@ -128,7 +127,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { ); ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); - for (int i = 0; i < APPLICATION_UPDATE_COUNT_LIMIT + 1; i++) { + for (int i = 0; i < APPLICATION_UPDATE_COUNT_LIMIT; i++) { applicationSubmissionService.apply(테스트유저_1, request); } From 90cee69d0dab4a652eff6906c37179d6e4f5818a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:20:28 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=EA=B2=80=EC=A6=9D=20api=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: SiteUser에서 MyPage로 서비스명 변경하여 역할 명확화 * feat: 닉네임 중복 검증 api 추가 * style: 컨트롤러 메서드 인자 개행 컨벤션 적용 * refactor: checkNicknameExists로 함수명 개선 --- .../custom/exception/ErrorCode.java | 2 +- .../siteuser/controller/MyPageController.java | 40 +++ .../controller/SiteUserController.java | 30 +- .../siteuser/dto/NicknameExistsResponse.java | 9 + .../siteuser/service/MyPageService.java | 105 +++++++ .../siteuser/service/SiteUserService.java | 91 +----- .../controller/UniversityController.java | 6 +- .../siteuser/service/MyPageServiceTest.java | 292 ++++++++++++++++++ .../siteuser/service/SiteUserServiceTest.java | 227 +------------- 9 files changed, 474 insertions(+), 328 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/NicknameExistsResponse.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java create mode 100644 src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 52bab0980..4b581f79c 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -5,7 +5,7 @@ import org.springframework.http.HttpStatus; import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; -import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.siteuser.service.MyPageService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; @Getter @AllArgsConstructor diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java new file mode 100644 index 000000000..94bc6e8a8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.siteuser.controller; + +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.service.MyPageService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@RequestMapping("/my") +@RestController +class MyPageController { + + private final MyPageService myPageService; + + @GetMapping + public ResponseEntity getMyPageInfo( + @AuthorizedUser SiteUser siteUser + ) { + MyPageResponse myPageResponse = myPageService.getMyPageInfo(siteUser); + return ResponseEntity.ok(myPageResponse); + } + + @PatchMapping + public ResponseEntity updateMyPageInfo( + @AuthorizedUser SiteUser siteUser, + @RequestParam("file") MultipartFile imageFile, + @RequestParam("nickname") String nickname + ) { + myPageService.updateMyPageInfo(siteUser, imageFile, nickname); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index 46f1555f1..64d926eca 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -1,40 +1,26 @@ package com.example.solidconnection.siteuser.controller; -import com.example.solidconnection.custom.resolver.AuthorizedUser; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.NicknameExistsResponse; import com.example.solidconnection.siteuser.service.SiteUserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor -@RequestMapping("/my") +@RequestMapping("/users") @RestController -class SiteUserController { +public class SiteUserController { private final SiteUserService siteUserService; - @GetMapping - public ResponseEntity getMyPageInfo( - @AuthorizedUser SiteUser siteUser + @GetMapping("/exists") + public ResponseEntity checkNicknameExists( + @RequestParam("nickname") String nickname ) { - MyPageResponse myPageResponse = siteUserService.getMyPageInfo(siteUser); - return ResponseEntity.ok(myPageResponse); - } - - @PatchMapping - public ResponseEntity updateMyPageInfo( - @AuthorizedUser SiteUser siteUser, - @RequestParam(value = "file", required = false) MultipartFile imageFile, - @RequestParam(value = "nickname", required = false) String nickname - ) { - siteUserService.updateMyPageInfo(siteUser, imageFile, nickname); - return ResponseEntity.ok().build(); + NicknameExistsResponse nicknameExistsResponse = siteUserService.checkNicknameExists(nickname); + return ResponseEntity.ok(nicknameExistsResponse); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameExistsResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameExistsResponse.java new file mode 100644 index 000000000..efb53df54 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameExistsResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.siteuser.dto; + +public record NicknameExistsResponse( + boolean exists +) { + public static NicknameExistsResponse from(boolean exists) { + return new NicknameExistsResponse(exists); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java new file mode 100644 index 000000000..50a020427 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -0,0 +1,105 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; + +@RequiredArgsConstructor +@Service +public class MyPageService { + + public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 7; + public static final DateTimeFormatter NICKNAME_LAST_CHANGE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + private final SiteUserRepository siteUserRepository; + private final LikedUniversityRepository likedUniversityRepository; + private final S3Service s3Service; + + /* + * 마이페이지 정보를 조회한다. + * */ + @Transactional(readOnly = true) + public MyPageResponse getMyPageInfo(SiteUser siteUser) { + int likedUniversityCount = likedUniversityRepository.countBySiteUser_Id(siteUser.getId()); + return MyPageResponse.of(siteUser, likedUniversityCount); + } + + /* + * 마이페이지 정보를 수정한다. + * */ + @Transactional + public void updateMyPageInfo(SiteUser siteUser, MultipartFile imageFile, String nickname) { + validateNicknameUnique(nickname); + validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); + validateProfileImageNotEmpty(imageFile); + + if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { + s3Service.deleteExProfile(siteUser); + } + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); + String profileImageUrl = uploadedFile.fileUrl(); + + siteUser.setProfileImageUrl(profileImageUrl); + siteUser.setNickname(nickname); + siteUser.setNicknameModifiedAt(LocalDateTime.now()); + siteUserRepository.save(siteUser); + } + + private void validateNicknameUnique(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { + if (lastModifiedAt == null) { + return; + } + if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { + String formatLastModifiedAt + = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(lastModifiedAt)); + throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); + } + } + + private void validateProfileImageNotEmpty(MultipartFile imageFile) { + if (imageFile == null || imageFile.isEmpty()) { + throw new CustomException(PROFILE_IMAGE_NEEDED); + } + } + + private boolean isDefaultProfileImage(String profileImageUrl) { + String prefix = "profile/"; + return profileImageUrl == null || !profileImageUrl.startsWith(prefix); + } + + /* + * 관심 대학교 목록을 조회한다. + * */ + @Transactional(readOnly = true) + public List getWishUniversity(SiteUser siteUser) { + List likedUniversities = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()); + return likedUniversities.stream() + .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) + .toList(); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index c2ee4ded7..e67d71ab8 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -1,101 +1,18 @@ package com.example.solidconnection.siteuser.service; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.s3.S3Service; -import com.example.solidconnection.s3.UploadedFileUrlResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.dto.NicknameExistsResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.ImgType; -import com.example.solidconnection.university.domain.LikedUniversity; -import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; @RequiredArgsConstructor @Service public class SiteUserService { - public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 7; - public static final DateTimeFormatter NICKNAME_LAST_CHANGE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - private final SiteUserRepository siteUserRepository; - private final LikedUniversityRepository likedUniversityRepository; - private final S3Service s3Service; - - /* - * 마이페이지 정보를 조회한다. - * */ - @Transactional(readOnly = true) - public MyPageResponse getMyPageInfo(SiteUser siteUser) { - int likedUniversityCount = likedUniversityRepository.countBySiteUser_Id(siteUser.getId()); - return MyPageResponse.of(siteUser, likedUniversityCount); - } - - /* - * 마이페이지 정보를 수정한다. - * */ - @Transactional - public void updateMyPageInfo(SiteUser siteUser, MultipartFile imageFile, String nickname) { - if (nickname != null) { - validateNicknameUnique(nickname); - validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); - siteUser.setNickname(nickname); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); - } - - if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); - if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { - s3Service.deleteExProfile(siteUser); - } - 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); - } - } - - private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { - if (lastModifiedAt == null) { - return; - } - if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { - String formatLastModifiedAt - = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(lastModifiedAt)); - throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); - } - } - - private boolean isDefaultProfileImage(String profileImageUrl) { - String prefix = "profile/"; - return profileImageUrl == null || !profileImageUrl.startsWith(prefix); - } - /* - * 관심 대학교 목록을 조회한다. - * */ - @Transactional(readOnly = true) - public List getWishUniversity(SiteUser siteUser) { - List likedUniversities = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()); - return likedUniversities.stream() - .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) - .toList(); + public NicknameExistsResponse checkNicknameExists(String nickname) { + boolean exists = siteUserRepository.existsByNickname(nickname); + return NicknameExistsResponse.from(exists); } } diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 83d90f600..635693d4c 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -2,7 +2,7 @@ import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.service.SiteUserService; +import com.example.solidconnection.siteuser.service.MyPageService; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.IsLikeResponse; import com.example.solidconnection.university.dto.LikeResultResponse; @@ -32,7 +32,7 @@ public class UniversityController { private final UniversityQueryService universityQueryService; private final UniversityLikeService universityLikeService; private final UniversityRecommendService universityRecommendService; - private final SiteUserService siteUserService; + private final MyPageService myPageService; @GetMapping("/recommend") public ResponseEntity getUniversityRecommends( @@ -49,7 +49,7 @@ public ResponseEntity getUniversityRecommends( public ResponseEntity> getMyWishUniversity( @AuthorizedUser SiteUser siteUser ) { - List wishUniversities = siteUserService.getWishUniversity(siteUser); + List wishUniversities = myPageService.getWishUniversity(siteUser); return ResponseEntity.ok(wishUniversities); } diff --git a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java new file mode 100644 index 000000000..6e897f04a --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java @@ -0,0 +1,292 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; +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.BDDMockito.any; +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; + +@DisplayName("마이페이지 서비스 테스트") +class MyPageServiceTest extends BaseIntegrationTest { + + @Autowired + private MyPageService myPageService; + + @MockBean + private S3Service s3Service; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Test + void 마이페이지_정보를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + int likedUniversityCount = createLikedUniversities(testUser); + + // when + MyPageResponse response = myPageService.getMyPageInfo(testUser); + + // then + Assertions.assertAll( + () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), + () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()), + () -> assertThat(response.role()).isEqualTo(testUser.getRole()), + () -> assertThat(response.birth()).isEqualTo(testUser.getBirth()), + () -> assertThat(response.email()).isEqualTo(testUser.getEmail()), + () -> assertThat(response.likedPostCount()).isEqualTo(testUser.getPostLikeList().size()), + () -> assertThat(response.likedUniversityCount()).isEqualTo(likedUniversityCount) + ); + } + + @Test + void 관심_대학교_목록을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + int likedUniversityCount = createLikedUniversities(testUser); + + // when + List response = myPageService.getWishUniversity(testUser); + + // then + assertThat(response) + .hasSize(likedUniversityCount) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("id") + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )); + } + + @Nested + class 프로필_이미지_수정_테스트 { + + @Test + void 새로운_이미지로_성공적으로_업데이트한다() { + // given + SiteUser testUser = createSiteUser(); + String expectedUrl = "newProfileImageUrl"; + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse(expectedUrl)); + + // when + myPageService.updateMyPageInfo(testUser, imageFile, "newNickname"); + + // then + assertThat(testUser.getProfileImageUrl()).isEqualTo(expectedUrl); + } + + @Test + void 프로필을_처음_수정하는_것이면_이전_이미지를_삭제하지_않는다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + + // when + myPageService.updateMyPageInfo(testUser, imageFile, "newNickname"); + + // then + then(s3Service).should(never()).deleteExProfile(any()); + } + + @Test + void 프로필을_처음_수정하는_것이_아니라면_이전_이미지를_삭제한다() { + // given + SiteUser testUser = createSiteUserWithCustomProfile(); + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + + // when + myPageService.updateMyPageInfo(testUser, imageFile, "newNickname"); + + // then + then(s3Service).should().deleteExProfile(testUser); + } + + @Test + void 빈_이미지_파일로_프로필을_수정하면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile emptyFile = createEmptyImageFile(); + + // when & then + assertThatCode(() -> myPageService.updateMyPageInfo(testUser, emptyFile, "newNickname")) + .isInstanceOf(CustomException.class) + .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); + } + } + + @Nested + class 닉네임_수정_테스트 { + + @BeforeEach + void setUp() { + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + } + + @Test + void 닉네임을_성공적으로_수정한다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); + String newNickname = "newNickname"; + + // when + myPageService.updateMyPageInfo(testUser, imageFile, newNickname); + + // then + SiteUser updatedUser = siteUserRepository.findById(testUser.getId()).get(); + assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); + assertThat(updatedUser.getNickname()).isEqualTo(newNickname); + } + + @Test + void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() { + // given + createDuplicatedSiteUser(); + SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); + + // when & then + assertThatCode(() -> myPageService.updateMyPageInfo(testUser, imageFile, "duplicatedNickname")) + .isInstanceOf(CustomException.class) + .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); + } + + @Test + void 최소_대기기간이_지나지_않은_상태에서_변경하면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); + LocalDateTime modifiedAt = LocalDateTime.now().minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES - 1); + testUser.setNicknameModifiedAt(modifiedAt); + siteUserRepository.save(testUser); + + NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); + + // when & then + assertThatCode(() -> myPageService.updateMyPageInfo(testUser, imageFile, "nickname12")) + .isInstanceOf(CustomException.class) + .hasMessage(createExpectedErrorMessage(modifiedAt)); + } + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private SiteUser createSiteUserWithCustomProfile() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profile/profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private void createDuplicatedSiteUser() { + SiteUser siteUser = new SiteUser( + "duplicated@example.com", + "duplicatedNickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + siteUserRepository.save(siteUser); + } + + private int createLikedUniversities(SiteUser testUser) { + LikedUniversity likedUniversity1 = new LikedUniversity(null, 괌대학_A_지원_정보, testUser); + LikedUniversity likedUniversity2 = new LikedUniversity(null, 메이지대학_지원_정보, testUser); + LikedUniversity likedUniversity3 = new LikedUniversity(null, 코펜하겐IT대학_지원_정보, testUser); + + likedUniversityRepository.save(likedUniversity1); + likedUniversityRepository.save(likedUniversity2); + likedUniversityRepository.save(likedUniversity3); + return likedUniversityRepository.countBySiteUser_Id(testUser.getId()); + } + + private MockMultipartFile createValidImageFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + private MockMultipartFile createEmptyImageFile() { + return new MockMultipartFile( + "image", + "empty.jpg", + "image/jpeg", + new byte[0] + ); + } + + private String createExpectedErrorMessage(LocalDateTime modifiedAt) { + String formatLastModifiedAt = String.format( + "(마지막 수정 시간 : %s)", + NICKNAME_LAST_CHANGE_DATE_FORMAT.format(modifiedAt) + ); + return CAN_NOT_CHANGE_NICKNAME_YET.getMessage() + " : " + formatLastModifiedAt; + } +} diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java index 819f33156..7272b3498 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -1,43 +1,19 @@ package com.example.solidconnection.siteuser.service; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.s3.S3Service; -import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.dto.NicknameExistsResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.ImgType; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; -import com.example.solidconnection.university.domain.LikedUniversity; -import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.mock.web.MockMultipartFile; -import java.time.LocalDateTime; -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; -import static com.example.solidconnection.siteuser.service.SiteUserService.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.BDDMockito.any; -import static org.mockito.BDDMockito.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.never; -import static org.mockito.BDDMockito.then; @DisplayName("유저 서비스 테스트") class SiteUserServiceTest extends BaseIntegrationTest { @@ -45,160 +21,35 @@ class SiteUserServiceTest extends BaseIntegrationTest { @Autowired private SiteUserService siteUserService; - @MockBean - private S3Service s3Service; - @Autowired private SiteUserRepository siteUserRepository; - @Autowired - private LikedUniversityRepository likedUniversityRepository; - - @Test - void 마이페이지_정보를_조회한다() { - // given - SiteUser testUser = createSiteUser(); - int likedUniversityCount = createLikedUniversities(testUser); - - // when - MyPageResponse response = siteUserService.getMyPageInfo(testUser); - - // then - Assertions.assertAll( - () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), - () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()), - () -> assertThat(response.role()).isEqualTo(testUser.getRole()), - () -> assertThat(response.authType()).isEqualTo(testUser.getAuthType()), - () -> assertThat(response.birth()).isEqualTo(testUser.getBirth()), - () -> assertThat(response.email()).isEqualTo(testUser.getEmail()), - () -> assertThat(response.likedPostCount()).isEqualTo(testUser.getPostLikeList().size()), - () -> assertThat(response.likedUniversityCount()).isEqualTo(likedUniversityCount) - ); - } - - @Test - void 관심_대학교_목록을_조회한다() { - // given - SiteUser testUser = createSiteUser(); - int likedUniversityCount = createLikedUniversities(testUser); + private SiteUser siteUser; - // when - List response = siteUserService.getWishUniversity(testUser); - - // then - assertThat(response) - .hasSize(likedUniversityCount) - .usingRecursiveFieldByFieldElementComparatorIgnoringFields("id") - .containsAll(List.of( - UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), - UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보), - UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) - )); + @BeforeEach + void setUp() { + siteUser = createSiteUser(); } @Nested - class 프로필_이미지_수정_테스트 { + class 닉네임_중복_검사 { @Test - void 새로운_이미지로_성공적으로_업데이트한다() { - // given - SiteUser testUser = createSiteUser(); - String expectedUrl = "newProfileImageUrl"; - MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) - .willReturn(new UploadedFileUrlResponse(expectedUrl)); - + void 존재하는_닉네임이면_true를_반환한다() { // when - siteUserService.updateMyPageInfo(testUser, imageFile, "newNickname"); + NicknameExistsResponse response = siteUserService.checkNicknameExists(siteUser.getNickname()); // then - assertThat(testUser.getProfileImageUrl()).isEqualTo(expectedUrl); + assertThat(response.exists()).isTrue(); } @Test - void 프로필을_처음_수정하는_것이면_이전_이미지를_삭제하지_않는다() { - // given - SiteUser testUser = createSiteUser(); - MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) - .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); - + void 존재하지_않는_닉네임이면_false를_반환한다() { // when - siteUserService.updateMyPageInfo(testUser, imageFile, "newNickname"); + NicknameExistsResponse response = siteUserService.checkNicknameExists("nonExistingNickname"); // then - then(s3Service).should(never()).deleteExProfile(any()); - } - - @Test - void 프로필을_처음_수정하는_것이_아니라면_이전_이미지를_삭제한다() { - // given - SiteUser testUser = createSiteUserWithCustomProfile(); - MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) - .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); - - // when - siteUserService.updateMyPageInfo(testUser, imageFile, "newNickname"); - - // then - then(s3Service).should().deleteExProfile(testUser); - } - } - - @Nested - class 닉네임_수정_테스트 { - - @BeforeEach - void setUp() { - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) - .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); - } - - @Test - void 닉네임을_성공적으로_수정한다() { - // given - SiteUser testUser = createSiteUser(); - MockMultipartFile imageFile = createValidImageFile(); - String newNickname = "newNickname"; - - // when - siteUserService.updateMyPageInfo(testUser, imageFile, newNickname); - - // then - SiteUser updatedUser = siteUserRepository.findById(testUser.getId()).get(); - assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); - assertThat(updatedUser.getNickname()).isEqualTo(newNickname); - } - - @Test - void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() { - // given - createDuplicatedSiteUser(); - SiteUser testUser = createSiteUser(); - MockMultipartFile imageFile = createValidImageFile(); - - // when & then - assertThatCode(() -> siteUserService.updateMyPageInfo(testUser, imageFile, "duplicatedNickname")) - .isInstanceOf(CustomException.class) - .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); - } - - @Test - void 최소_대기기간이_지나지_않은_상태에서_변경하면_예외_응답을_반환한다() { - // given - SiteUser testUser = createSiteUser(); - MockMultipartFile imageFile = createValidImageFile(); - LocalDateTime modifiedAt = LocalDateTime.now().minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES - 1); - testUser.setNicknameModifiedAt(modifiedAt); - siteUserRepository.save(testUser); - - NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); - - // when & then - assertThatCode(() -> siteUserService.updateMyPageInfo(testUser, imageFile, "nickname12")) - .isInstanceOf(CustomException.class) - .hasMessage(createExpectedErrorMessage(modifiedAt)); + assertThat(response.exists()).isFalse(); } } @@ -214,58 +65,4 @@ private SiteUser createSiteUser() { ); return siteUserRepository.save(siteUser); } - - private SiteUser createSiteUserWithCustomProfile() { - SiteUser siteUser = new SiteUser( - "test@example.com", - "nickname", - "profile/profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - return siteUserRepository.save(siteUser); - } - - private void createDuplicatedSiteUser() { - SiteUser siteUser = new SiteUser( - "duplicated@example.com", - "duplicatedNickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - siteUserRepository.save(siteUser); - } - - private int createLikedUniversities(SiteUser testUser) { - LikedUniversity likedUniversity1 = new LikedUniversity(null, 괌대학_A_지원_정보, testUser); - LikedUniversity likedUniversity2 = new LikedUniversity(null, 메이지대학_지원_정보, testUser); - LikedUniversity likedUniversity3 = new LikedUniversity(null, 코펜하겐IT대학_지원_정보, testUser); - - likedUniversityRepository.save(likedUniversity1); - likedUniversityRepository.save(likedUniversity2); - likedUniversityRepository.save(likedUniversity3); - return likedUniversityRepository.countBySiteUser_Id(testUser.getId()); - } - - private MockMultipartFile createValidImageFile() { - return new MockMultipartFile( - "image", - "test.jpg", - "image/jpeg", - "test image content".getBytes() - ); - } - - private String createExpectedErrorMessage(LocalDateTime modifiedAt) { - String formatLastModifiedAt = String.format( - "(마지막 수정 시간 : %s)", - NICKNAME_LAST_CHANGE_DATE_FORMAT.format(modifiedAt) - ); - return CAN_NOT_CHANGE_NICKNAME_YET.getMessage() + " : " + formatLastModifiedAt; - } } From 2117baa6774bcf9778165603ff966d0ce52affe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 8 Mar 2025 09:16:50 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=ED=98=84=ED=99=A9=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EB=A7=8C=20=EB=B3=BC=20=EC=88=98=20=EC=9E=88=EA=B2=8C?= =?UTF-8?q?=20aop=20=EC=A0=81=EC=9A=A9=20(#254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 2 + .../annotation/RequireAdminAccess.java | 11 +++ .../aspect/AdminAuthorizationAspect.java | 35 +++++++ .../aspect/AdminAuthorizationAspectTest.java | 99 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/custom/security/annotation/RequireAdminAccess.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspect.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspectTest.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 801cbf10c..36c7d6af2 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -6,6 +6,7 @@ import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.application.service.ApplicationSubmissionService; import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.custom.security.annotation.RequireAdminAccess; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -38,6 +39,7 @@ public ResponseEntity apply( .body(applicationSubmissionResponse); } + @RequireAdminAccess @GetMapping public ResponseEntity getApplicants( @AuthorizedUser SiteUser siteUser, diff --git a/src/main/java/com/example/solidconnection/custom/security/annotation/RequireAdminAccess.java b/src/main/java/com/example/solidconnection/custom/security/annotation/RequireAdminAccess.java new file mode 100644 index 000000000..559664e25 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/annotation/RequireAdminAccess.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.custom.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireAdminAccess { +} diff --git a/src/main/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspect.java b/src/main/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspect.java new file mode 100644 index 000000000..20e8c27c8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspect.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.custom.security.aspect; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.security.annotation.RequireAdminAccess; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED; +import static com.example.solidconnection.type.Role.ADMIN; + +@Aspect +@Component +@RequiredArgsConstructor +public class AdminAuthorizationAspect { + + @Around("@annotation(requireAdminAccess)") + public Object checkAdminAccess(ProceedingJoinPoint joinPoint, + RequireAdminAccess requireAdminAccess) throws Throwable { + SiteUser siteUser = null; + for (Object arg : joinPoint.getArgs()) { + if (arg instanceof SiteUser) { + siteUser = (SiteUser) arg; + break; + } + } + if (siteUser == null || !ADMIN.equals(siteUser.getRole())) { + throw new CustomException(ACCESS_DENIED); + } + return joinPoint.proceed(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspectTest.java b/src/test/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspectTest.java new file mode 100644 index 000000000..2996eb5b6 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/aspect/AdminAuthorizationAspectTest.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.custom.security.aspect; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.security.annotation.RequireAdminAccess; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +@TestContainerSpringBootTest +@DisplayName("어드민 권한 검사 Aspect 테스트") +class AdminAuthorizationAspectTest { + + @Autowired + private TestService testService; + + @Test + void 어드민_사용자는_어드민_전용_메소드에_접근할_수_있다() { + // given + SiteUser adminUser = createSiteUser(Role.ADMIN); + + // when + boolean response = testService.adminOnlyMethod(adminUser); + + // then + assertThat(response).isTrue(); + } + + @Test + void 일반_사용자가_어드민_전용_메소드에_접근하면_예외_응답을_반환한다() { + // given + SiteUser mentorUser = createSiteUser(Role.MENTOR); + + // when & then + assertThatCode(() -> testService.adminOnlyMethod(mentorUser)) + .isInstanceOf(CustomException.class) + .hasMessage(ACCESS_DENIED.getMessage()); + } + + @Test + void 어드민_어노테이션이_없는_메소드는_모두_접근_가능하다() { + // given + SiteUser menteeUser = createSiteUser(Role.MENTEE); + SiteUser adminUser = createSiteUser(Role.ADMIN); + + // when + boolean menteeResponse = testService.publicMethod(menteeUser); + boolean adminResponse = testService.publicMethod(adminUser); + + // then + assertThat(menteeResponse).isTrue(); + assertThat(adminResponse).isTrue(); + } + + private SiteUser createSiteUser(Role role) { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + role, + Gender.MALE + ); + } + + @TestConfiguration + static class TestConfig { + + @Bean + public TestService testService() { + return new TestService(); + } + } + + @Component + static class TestService { + + @RequireAdminAccess + public boolean adminOnlyMethod(SiteUser siteUser) { + return true; + } + + public boolean publicMethod(SiteUser siteUser) { + return true; + } + } +} From 1bce344f0b21648f4e47f7a3cccafd944a253f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:12:38 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20=EC=9E=84=EB=B2=A0=EB=94=94?= =?UTF-8?q?=EB=93=9C=20=ED=83=80=EC=9E=85=20DTO=EC=97=90=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=EB=85=B8=EC=B6=9C=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20DTO=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=20=ED=86=B5=EC=9D=BC=20(#261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: GPA 대신 GpaResponse DTO로 응답 구조 개선 * refactor: LanguageTest 대신 LanguageTestResponse DTO로 응답 구조 개선 --- .../score/controller/ScoreController.java | 12 ++++---- .../score/dto/GpaResponse.java | 17 +++++++++++ .../score/dto/GpaScoreRequest.java | 1 + .../score/dto/GpaScoreStatus.java | 21 -------------- .../score/dto/GpaScoreStatusResponse.java | 16 +++++++++-- .../score/dto/GpaScoreStatusesResponse.java | 11 ++++++++ .../score/dto/LanguageTestResponse.java | 18 ++++++++++++ .../score/dto/LanguageTestScoreRequest.java | 1 + .../score/dto/LanguageTestScoreStatus.java | 21 -------------- .../dto/LanguageTestScoreStatusResponse.java | 16 +++++++++-- .../LanguageTestScoreStatusesResponse.java | 11 ++++++++ .../score/service/ScoreService.java | 20 ++++++------- .../score/service/ScoreServiceTest.java | 28 +++++++++---------- 13 files changed, 117 insertions(+), 76 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/score/dto/GpaResponse.java delete mode 100644 src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusesResponse.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/LanguageTestResponse.java delete mode 100644 src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusesResponse.java diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java index 4ea560657..e67639274 100644 --- a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -2,9 +2,9 @@ import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.score.dto.GpaScoreRequest; -import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.GpaScoreStatusesResponse; import com.example.solidconnection.score.dto.LanguageTestScoreRequest; -import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusesResponse; import com.example.solidconnection.score.service.ScoreService; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; @@ -49,19 +49,19 @@ public ResponseEntity submitLanguageTestScore( // 학점 상태를 확인하는 api @GetMapping("/gpas") - public ResponseEntity getGpaScoreStatus( + public ResponseEntity getGpaScoreStatus( @AuthorizedUser SiteUser siteUser ) { - GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser); + GpaScoreStatusesResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser); return ResponseEntity.ok(gpaScoreStatus); } // 어학 성적 상태를 확인하는 api @GetMapping("/language-tests") - public ResponseEntity getLanguageTestScoreStatus( + public ResponseEntity getLanguageTestScoreStatus( @AuthorizedUser SiteUser siteUser ) { - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser); + LanguageTestScoreStatusesResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser); return ResponseEntity.ok(languageTestScoreStatus); } } diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaResponse.java b/src/main/java/com/example/solidconnection/score/dto/GpaResponse.java new file mode 100644 index 000000000..fc05667aa --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/GpaResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.score.dto; + +import com.example.solidconnection.application.domain.Gpa; + +public record GpaResponse( + double gpa, + double gpaCriteria, + String gpaReportUrl +) { + public static GpaResponse from(Gpa gpa) { + return new GpaResponse( + gpa.getGpa(), + gpa.getGpaCriteria(), + gpa.getGpaReportUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java index beafbf2e3..a95cced6c 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotNull; public record GpaScoreRequest( + @NotNull(message = "학점을 입력해주세요.") Double gpa, diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java deleted file mode 100644 index 5798e3cf0..000000000 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.solidconnection.score.dto; - -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.type.VerifyStatus; - -public record GpaScoreStatus( - Long id, - Gpa gpa, - VerifyStatus verifyStatus, - String rejectedReason -) { - public static GpaScoreStatus from(GpaScore gpaScore) { - return new GpaScoreStatus( - gpaScore.getId(), - gpaScore.getGpa(), - gpaScore.getVerifyStatus(), - gpaScore.getRejectedReason() - ); - } -} diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java index 06fdba0d3..df161d358 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java @@ -1,8 +1,20 @@ package com.example.solidconnection.score.dto; -import java.util.List; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.type.VerifyStatus; public record GpaScoreStatusResponse( - List gpaScoreStatusList + long id, + GpaResponse gpaResponse, + VerifyStatus verifyStatus, + String rejectedReason ) { + public static GpaScoreStatusResponse from(GpaScore gpaScore) { + return new GpaScoreStatusResponse( + gpaScore.getId(), + GpaResponse.from(gpaScore.getGpa()), + gpaScore.getVerifyStatus(), + gpaScore.getRejectedReason() + ); + } } diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusesResponse.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusesResponse.java new file mode 100644 index 000000000..24a12af18 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusesResponse.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.score.dto; + +import java.util.List; + +public record GpaScoreStatusesResponse( + List gpaScoreStatusResponseList +) { + public static GpaScoreStatusesResponse from(List gpaScoreStatusResponseList) { + return new GpaScoreStatusesResponse(gpaScoreStatusResponseList); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestResponse.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestResponse.java new file mode 100644 index 000000000..060574f46 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestResponse.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.score.dto; + +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.type.LanguageTestType; + +public record LanguageTestResponse( + LanguageTestType languageTestType, + String languageTestScore, + String languageTestReportUrl +) { + public static LanguageTestResponse from(LanguageTest languageTest) { + return new LanguageTestResponse( + languageTest.getLanguageTestType(), + languageTest.getLanguageTestScore(), + languageTest.getLanguageTestReportUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java index de9329898..e49af4369 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java @@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotNull; public record LanguageTestScoreRequest( + @NotNull(message = "어학 종류를 입력해주세요.") LanguageTestType languageTestType, diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java deleted file mode 100644 index 9e5fcae4f..000000000 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.solidconnection.score.dto; - -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.type.VerifyStatus; - -public record LanguageTestScoreStatus( - Long id, - LanguageTest languageTest, - VerifyStatus verifyStatus, - String rejectedReason -) { - public static LanguageTestScoreStatus from(LanguageTestScore languageTestScore) { - return new LanguageTestScoreStatus( - languageTestScore.getId(), - languageTestScore.getLanguageTest(), - languageTestScore.getVerifyStatus(), - languageTestScore.getRejectedReason() - ); - } -} diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java index e19c0e855..3ee96906e 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java @@ -1,8 +1,20 @@ package com.example.solidconnection.score.dto; -import java.util.List; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.type.VerifyStatus; public record LanguageTestScoreStatusResponse( - List languageTestScoreStatusList + long id, + LanguageTestResponse languageTestResponse, + VerifyStatus verifyStatus, + String rejectedReason ) { + public static LanguageTestScoreStatusResponse from(LanguageTestScore languageTestScore) { + return new LanguageTestScoreStatusResponse( + languageTestScore.getId(), + LanguageTestResponse.from(languageTestScore.getLanguageTest()), + languageTestScore.getVerifyStatus(), + languageTestScore.getRejectedReason() + ); + } } diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusesResponse.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusesResponse.java new file mode 100644 index 000000000..027794853 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusesResponse.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.score.dto; + +import java.util.List; + +public record LanguageTestScoreStatusesResponse( + List languageTestScoreStatusResponseList +) { + public static LanguageTestScoreStatusesResponse from(List languageTestScoreStatusResponseList) { + return new LanguageTestScoreStatusesResponse(languageTestScoreStatusResponseList); + } +} diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index 66592d339..eb00a14e9 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -8,11 +8,11 @@ import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.score.dto.GpaScoreRequest; -import com.example.solidconnection.score.dto.GpaScoreStatus; import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.GpaScoreStatusesResponse; import com.example.solidconnection.score.dto.LanguageTestScoreRequest; -import com.example.solidconnection.score.dto.LanguageTestScoreStatus; import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusesResponse; import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -75,28 +75,28 @@ public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest } @Transactional(readOnly = true) - public GpaScoreStatusResponse getGpaScoreStatus(SiteUser siteUser) { + public GpaScoreStatusesResponse getGpaScoreStatus(SiteUser siteUser) { // todo: ditto SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - List gpaScoreStatusList = + List gpaScoreStatusResponseList = Optional.ofNullable(siteUser1.getGpaScoreList()) .map(scores -> scores.stream() - .map(GpaScoreStatus::from) + .map(GpaScoreStatusResponse::from) .collect(Collectors.toList())) .orElse(Collections.emptyList()); - return new GpaScoreStatusResponse(gpaScoreStatusList); + return GpaScoreStatusesResponse.from(gpaScoreStatusResponseList); } @Transactional(readOnly = true) - public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(SiteUser siteUser) { + public LanguageTestScoreStatusesResponse getLanguageTestScoreStatus(SiteUser siteUser) { // todo: ditto SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - List languageTestScoreStatusList = + List languageTestScoreStatusResponseList = Optional.ofNullable(siteUser1.getLanguageTestScoreList()) .map(scores -> scores.stream() - .map(LanguageTestScoreStatus::from) + .map(LanguageTestScoreStatusResponse::from) .collect(Collectors.toList())) .orElse(Collections.emptyList()); - return new LanguageTestScoreStatusResponse(languageTestScoreStatusList); + return LanguageTestScoreStatusesResponse.from(languageTestScoreStatusResponseList); } } diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 0617e1c25..8d2054579 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -7,11 +7,11 @@ import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.score.dto.GpaScoreRequest; -import com.example.solidconnection.score.dto.GpaScoreStatus; import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.GpaScoreStatusesResponse; import com.example.solidconnection.score.dto.LanguageTestScoreRequest; -import com.example.solidconnection.score.dto.LanguageTestScoreStatus; import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusesResponse; import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -63,15 +63,15 @@ class ScoreServiceTest extends BaseIntegrationTest { ); // when - GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser); + GpaScoreStatusesResponse response = scoreService.getGpaScoreStatus(testUser); // then - assertThat(response.gpaScoreStatusList()) + assertThat(response.gpaScoreStatusResponseList()) .hasSize(scores.size()) .containsExactlyInAnyOrder( scores.stream() - .map(GpaScoreStatus::from) - .toArray(GpaScoreStatus[]::new) + .map(GpaScoreStatusResponse::from) + .toArray(GpaScoreStatusResponse[]::new) ); } @@ -81,10 +81,10 @@ class ScoreServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser); + GpaScoreStatusesResponse response = scoreService.getGpaScoreStatus(testUser); // then - assertThat(response.gpaScoreStatusList()).isEmpty(); + assertThat(response.gpaScoreStatusResponseList()).isEmpty(); } @Test @@ -98,15 +98,15 @@ class ScoreServiceTest extends BaseIntegrationTest { siteUserRepository.save(testUser); // when - LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser); + LanguageTestScoreStatusesResponse response = scoreService.getLanguageTestScoreStatus(testUser); // then - assertThat(response.languageTestScoreStatusList()) + assertThat(response.languageTestScoreStatusResponseList()) .hasSize(scores.size()) .containsExactlyInAnyOrder( scores.stream() - .map(LanguageTestScoreStatus::from) - .toArray(LanguageTestScoreStatus[]::new) + .map(LanguageTestScoreStatusResponse::from) + .toArray(LanguageTestScoreStatusResponse[]::new) ); } @@ -116,10 +116,10 @@ class ScoreServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser); + LanguageTestScoreStatusesResponse response = scoreService.getLanguageTestScoreStatus(testUser); // then - assertThat(response.languageTestScoreStatusList()).isEmpty(); + assertThat(response.languageTestScoreStatusResponseList()).isEmpty(); } @Test From 6dfc6ed7ebd3f859fa5f482fc5d073d43eb5e0d4 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 2 Apr 2025 16:49:52 +0900 Subject: [PATCH 5/8] =?UTF-8?q?chore:=20ci=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20ci=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: ci 스크립트 작성 * chore: 테스트용 application.yml 분리 * test: test profile 제거 * refactor: 애플 시크릿키를 yml로 이동 * refactor: 함수 이름 변경 * refactor: 오타 수정 * refactor: redis testcontainers 설정 수정 * refactor: ddl-auto 옵션 변경 - create-drop을 create로 변경 - 테스트 컨테이너를 실행해서 테스트하기 때문에, 테스트가 종료되면 당연히 테이블이 삭제된다. 따라서 create-drop을 하는것과 create를 하는 것이 동일하게 동작한다. 굳이 create로 바꾼 것은 삭제 시 FK 위반으로 WARN 로그가 불필요하게 백몇줄이 찍히는걸 막기 위해서. * test: e2e 테스트 수정 - 깨지는 테스트 수정 - 비지니스 로직 변경으로 더 이상 해당되지 않는 테스트 삭제 * chore: createAt과 updateAt의 나노초 길이 고정 * style: 주석 추가 * refactor: MySQL 테스트 컨테이너 설정 변경 - redis 테스트 컨테이너와 동일하게 설정하여 불필요한 인지 부하를 줄인다. --- .github/workflows/ci.yml | 37 ++++++++ .../AppleOAuthClientSecretProvider.java | 22 ++--- .../client/AppleOAuthClientProperties.java | 3 +- .../entity/common/BaseEntity.java | 8 +- .../database/DatabaseConnectionTest.java | 2 - .../database/RedisConnectionTest.java | 2 - .../e2e/ApplicantsQueryTest.java | 88 ++++--------------- .../support/DatabaseCleaner.java | 2 - .../support/MySQLTestContainer.java | 36 +++----- .../support/RedisTestContainer.java | 33 ++++--- .../support/TestContainerDataJpaTest.java | 6 +- .../support/TestContainerSpringBootTest.java | 6 +- src/test/resources/application.yml | 73 +++++++++++++++ 13 files changed, 175 insertions(+), 143 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 src/test/resources/application.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..794b49827 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI with Gradle + +on: + pull_request: + branches: [ "develop", "release", "master" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Build with Gradle Wrapper + run: ./gradlew build + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java index 2de0b7291..31228e5d3 100644 --- a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java @@ -9,20 +9,17 @@ import org.apache.tomcat.util.codec.binary.Base64; import org.springframework.stereotype.Component; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Date; -import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY; /* - * 애플 OAuth 에 필요하 클라이언트 시크릿은 매번 동적으로 생성해야 한다. + * 애플 OAuth 에 필요한 클라이언트 시크릿은 매번 동적으로 생성해야 한다. * 클라이언트 시크릿은 애플 개발자 계정에서 발급받은 개인키(*.p8)를 사용하여 JWT 를 생성한다. * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret * */ @@ -32,14 +29,13 @@ public class AppleOAuthClientSecretProvider { private static final String KEY_ID_HEADER = "kid"; private static final long TOKEN_DURATION = 1000 * 60 * 10; // 10min - private static final String SECRET_KEY_PATH = "secret/AppleOAuthKey.p8"; private final AppleOAuthClientProperties appleOAuthClientProperties; private PrivateKey privateKey; @PostConstruct private void initPrivateKey() { - privateKey = readPrivateKey(); + privateKey = loadPrivateKey(); } public String generateClientSecret() { @@ -57,16 +53,14 @@ public String generateClientSecret() { .compact(); } - private PrivateKey readPrivateKey() { - try (InputStream is = getClass().getClassLoader().getResourceAsStream(SECRET_KEY_PATH); - BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - - String secretKey = reader.lines().collect(Collectors.joining("\n")); + private PrivateKey loadPrivateKey() { + try { + String secretKey = appleOAuthClientProperties.secretKey(); byte[] encoded = Base64.decodeBase64(secretKey); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); KeyFactory keyFactory = KeyFactory.getInstance("EC"); return keyFactory.generatePrivate(keySpec); - } catch (Exception e) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY); } } diff --git a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java index 609e9ee89..c04908583 100644 --- a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java +++ b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java @@ -10,6 +10,7 @@ public record AppleOAuthClientProperties( String publicKeyUrl, String clientId, String teamId, - String keyId + String keyId, + String secretKey ) { } diff --git a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java index 27493f1be..508953f88 100644 --- a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java +++ b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java @@ -9,9 +9,11 @@ import org.hibernate.annotations.DynamicUpdate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.ZoneId; import java.time.ZonedDateTime; +import static java.time.ZoneOffset.UTC; +import static java.time.temporal.ChronoUnit.MICROS; + @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Getter @@ -24,12 +26,12 @@ public abstract class BaseEntity { @PrePersist public void onPrePersist() { - this.createdAt = ZonedDateTime.now(ZoneId.of("UTC")); + this.createdAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); // 나노초 6자리 까지만 저장 this.updatedAt = this.createdAt; } @PreUpdate public void onPreUpdate() { - this.updatedAt = ZonedDateTime.now(ZoneId.of("UTC")); + this.updatedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); } } diff --git a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java index d156cf485..ca3c64c7a 100644 --- a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java @@ -8,7 +8,6 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; import java.sql.DatabaseMetaData; import java.sql.SQLException; @@ -20,7 +19,6 @@ @Disabled @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY) -@ActiveProfiles("test") @DataJpaTest class DatabaseConnectionTest { diff --git a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java index 69fcedaef..527ae7e07 100644 --- a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java @@ -6,12 +6,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.test.context.ActiveProfiles; import static org.assertj.core.api.Assertions.assertThat; @Disabled -@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class RedisConnectionTest { diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 868eac179..03d130542 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -105,7 +105,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/applications") + .get("/applications/competitors") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -119,30 +119,24 @@ public void setUpUserAndToken() { List.of(ApplicantResponse.of(사용자1_지원정보, false))), UniversityApplicantsResponse.of(괌대학_B_지원_정보, List.of(ApplicantResponse.of(나의_지원정보, true))), - UniversityApplicantsResponse.of(메이지대학_지원_정보, - List.of(ApplicantResponse.of(사용자2_지원정보, false))), - UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, - List.of(ApplicantResponse.of(사용자3_지원정보, false))) + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of()) )); assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of( UniversityApplicantsResponse.of(괌대학_A_지원_정보, - List.of(ApplicantResponse.of(나의_지원정보, true))), + List.of(ApplicantResponse.of(나의_지원정보, false))), UniversityApplicantsResponse.of(괌대학_B_지원_정보, - List.of(ApplicantResponse.of(사용자1_지원정보, false))), - UniversityApplicantsResponse.of(메이지대학_지원_정보, - List.of(ApplicantResponse.of(사용자3_지원정보, false))), - UniversityApplicantsResponse.of(그라츠대학_지원_정보, - List.of(ApplicantResponse.of(사용자2_지원정보, false))) + List.of(ApplicantResponse.of(사용자1_지원정보, true))), + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of()) )); assertThat(thirdChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of()), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of()), UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, - List.of(ApplicantResponse.of(나의_지원정보, true))), - UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보, - List.of(ApplicantResponse.of(사용자2_지원정보, false))), - UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, - List.of(ApplicantResponse.of(사용자1_지원정보, false))), - UniversityApplicantsResponse.of(메이지대학_지원_정보, - List.of(ApplicantResponse.of(사용자3_지원정보, false))) + List.of(ApplicantResponse.of(나의_지원정보, true))) )); } @@ -151,7 +145,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/applications?region=" + 영미권.getCode()) + .get("/applications/competitors?region=" + 영미권.getCode()) .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -163,60 +157,14 @@ public void setUpUserAndToken() { UniversityApplicantsResponse.of(괌대학_A_지원_정보, List.of(ApplicantResponse.of(사용자1_지원정보, false))), UniversityApplicantsResponse.of(괌대학_B_지원_정보, - List.of(ApplicantResponse.of(나의_지원정보, true))), - UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, - List.of(ApplicantResponse.of(사용자3_지원정보, false))))); + List.of(ApplicantResponse.of(나의_지원정보, true))) + )); assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of( UniversityApplicantsResponse.of(괌대학_A_지원_정보, List.of(ApplicantResponse.of(나의_지원정보, true))), UniversityApplicantsResponse.of(괌대학_B_지원_정보, - List.of(ApplicantResponse.of(사용자1_지원정보, false))))); - } - - @Test - void 대학_국문_이름으로_필터링해서_지원자를_조회한다() { - ApplicationsResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().log().all() - .get("/applications?keyword=라") - .then().log().all() - .statusCode(200) - .extract().as(ApplicationsResponse.class); - - List firstChoiceApplicants = response.firstChoice(); - List secondChoiceApplicants = response.secondChoice(); - - assertThat(firstChoiceApplicants).containsExactlyInAnyOrder( - UniversityApplicantsResponse.of(그라츠대학_지원_정보, List.of()), - UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, List.of()), - UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, - List.of(ApplicantResponse.of(사용자3_지원정보, false)))); - assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of( - UniversityApplicantsResponse.of(그라츠대학_지원_정보, - List.of(ApplicantResponse.of(사용자2_지원정보, false))), - UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, - List.of(ApplicantResponse.of(사용자3_지원정보, false))), - UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, List.of()))); - } - - @Test - void 국가_국문_이름으로_필터링해서_지원자를_조회한다() { - ApplicationsResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().log().all() - .get("/applications?keyword=일본") - .then().log().all() - .statusCode(200) - .extract().as(ApplicationsResponse.class); - - List firstChoiceApplicants = response.firstChoice(); - List secondChoiceApplicants = response.secondChoice(); - - assertThat(firstChoiceApplicants).containsExactlyInAnyOrder( - UniversityApplicantsResponse.of(메이지대학_지원_정보, - List.of(ApplicantResponse.of(사용자2_지원정보, false)))); - assertThat(secondChoiceApplicants).containsExactlyInAnyOrder( - UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of())); + List.of(ApplicantResponse.of(사용자1_지원정보, false))) + )); } @Test @@ -224,7 +172,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/applications") + .get("/applications/competitors") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); diff --git a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java index bb77f82f2..aee6a2bc6 100644 --- a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java +++ b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java @@ -5,13 +5,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; -@ActiveProfiles("test") @Component public class DatabaseCleaner { diff --git a/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java b/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java index 0256fec13..d4e25ccde 100644 --- a/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java +++ b/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java @@ -1,34 +1,24 @@ package com.example.solidconnection.support; -import jakarta.annotation.PostConstruct; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import javax.sql.DataSource; +public class MySQLTestContainer implements ApplicationContextInitializer { -@TestConfiguration -public class MySQLTestContainer { - - @Container private static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:8.0"); - @Bean - public DataSource dataSource() { - return DataSourceBuilder.create() - .url(CONTAINER.getJdbcUrl()) - .username(CONTAINER.getUsername()) - .password(CONTAINER.getPassword()) - .driverClassName(CONTAINER.getDriverClassName()) - .build(); + static { + CONTAINER.start(); } - @PostConstruct - void startContainer() { - if (!CONTAINER.isRunning()) { - CONTAINER.start(); - } + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertyValues.of( + "spring.datasource.url=" + CONTAINER.getJdbcUrl(), + "spring.datasource.username=" + CONTAINER.getUsername(), + "spring.datasource.password=" + CONTAINER.getPassword() + ).applyTo(applicationContext.getEnvironment()); } } diff --git a/src/test/java/com/example/solidconnection/support/RedisTestContainer.java b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java index 39f35c2d5..212499361 100644 --- a/src/test/java/com/example/solidconnection/support/RedisTestContainer.java +++ b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java @@ -1,28 +1,25 @@ package com.example.solidconnection.support; -import jakarta.annotation.PostConstruct; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; -@TestConfiguration -public class RedisTestContainer { +public class RedisTestContainer implements ApplicationContextInitializer { - @Container - private static final GenericContainer CONTAINER = new GenericContainer<>("redis:7.0"); + private static final int ORIGINAL_PORT = 6379; + private static final GenericContainer CONTAINER = new GenericContainer<>("redis:7.0") + .withExposedPorts(ORIGINAL_PORT); - @DynamicPropertySource - static void redisProperties(DynamicPropertyRegistry registry) { - registry.add("spring.redis.host", CONTAINER::getHost); - registry.add("spring.redis.port", CONTAINER::getFirstMappedPort); + static { + CONTAINER.start(); } - @PostConstruct - void startContainer() { - if (!CONTAINER.isRunning()) { - CONTAINER.start(); - } + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertyValues.of( + "spring.data.redis.host=" + CONTAINER.getHost(), + "spring.data.redis.port=" + CONTAINER.getMappedPort(ORIGINAL_PORT) + ).applyTo(applicationContext.getEnvironment()); } } diff --git a/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java index 339672e60..415b21e78 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java @@ -2,8 +2,7 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.testcontainers.junit.jupiter.Testcontainers; import java.lang.annotation.ElementType; @@ -13,9 +12,8 @@ @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@ActiveProfiles("test") @Testcontainers -@Import(MySQLTestContainer.class) +@ContextConfiguration(initializers = MySQLTestContainer.class) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TestContainerDataJpaTest { diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java index fe9b74f60..5c5c93742 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -3,8 +3,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.testcontainers.junit.jupiter.Testcontainers; import java.lang.annotation.ElementType; @@ -13,11 +12,10 @@ import java.lang.annotation.Target; @ExtendWith({DatabaseClearExtension.class}) +@ContextConfiguration(initializers = {RedisTestContainer.class, MySQLTestContainer.class}) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@ActiveProfiles("test") @Testcontainers -@Import({MySQLTestContainer.class, RedisTestContainer.class}) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TestContainerSpringBootTest { diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 000000000..7c6f83171 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,73 @@ +spring: + +# db + data: + redis: + host: localhost + port: 6379 + jpa: + hibernate: + ddl-auto: create + generate-ddl: true + show-sql: true + database: mysql + properties: + hibernate: + format_sql: true + flyway: + enabled: false + +# cloud +cloud: + aws: + credentials: + access-key: access-key + secret-key: access-key + region: + static: ap-northeast-2 + stack: + auto: false + s3: + bucket: solid-connection-uploaded + url: + default: default-url + uploaded: uploaded-url + cloudFront: + url: + default: default-url + uploaded: uploaded-url + +# variable +view: + count: + scheduling: + delay: 3000 +oauth: + apple: + token-url: "https://appleid.apple.com/auth/token" + client-secret-audience-url: "https://appleid.apple.com" + public-key-url: "https://appleid.apple.com/auth/keys" + client-id: client-id + team-id: team-id + key-id: key-id + redirect-url: "https://localhost:8080/auth/apple" + secret-key: MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCAfGIQ3TtNYAZG7i3m72odmdhfymkM9wAFg2rEL2RKUEA== # base64 encoded 된 임의의 값 +kakao: + redirect-url: "http://localhost:8080/auth/kakao" + client-id: client-id + token-url: "https://kauth.kakao.com/oauth/token" + user-info_url: "https://kapi.kakao.com/v2/user/me" +sentry: + environment: test + dsn: "https://test-public-key@sentry.test-domain.io/123456" + send-default-pii: true + traces-sample-rate: 1.0 + exception-resolver-order: -2147483647 +university: + term: 2024-1 +jwt: + secret: + 1234567-1234-1234-1234-12345678901 +cors: + allowed-origins: + - "http://localhost:8080" From a2495ea112fa67e21ef2ff012ef2bed7c12e7dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:48:37 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=EC=97=90=EC=84=9C=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84,=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../siteuser/controller/MyPageController.java | 4 +-- .../siteuser/service/MyPageService.java | 31 ++++++++----------- .../siteuser/service/MyPageServiceTest.java | 22 ------------- 3 files changed, 15 insertions(+), 42 deletions(-) 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 94bc6e8a8..41862bf8b 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -31,8 +31,8 @@ public ResponseEntity getMyPageInfo( @PatchMapping public ResponseEntity updateMyPageInfo( @AuthorizedUser SiteUser siteUser, - @RequestParam("file") MultipartFile imageFile, - @RequestParam("nickname") String nickname + @RequestParam(value = "file", required = false) MultipartFile imageFile, + @RequestParam(value = "nickname", required = false) String nickname ) { myPageService.updateMyPageInfo(siteUser, imageFile, nickname); return ResponseEntity.ok().build(); 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 50a020427..e89c6cdfa 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -21,7 +21,6 @@ import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; @RequiredArgsConstructor @Service @@ -48,19 +47,21 @@ public MyPageResponse getMyPageInfo(SiteUser siteUser) { * */ @Transactional public void updateMyPageInfo(SiteUser siteUser, MultipartFile imageFile, String nickname) { - validateNicknameUnique(nickname); - validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); - validateProfileImageNotEmpty(imageFile); - - if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { - s3Service.deleteExProfile(siteUser); + if (nickname != null) { + validateNicknameUnique(nickname); + validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); + siteUser.setNickname(nickname); + siteUser.setNicknameModifiedAt(LocalDateTime.now()); } - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); - String profileImageUrl = uploadedFile.fileUrl(); - siteUser.setProfileImageUrl(profileImageUrl); - siteUser.setNickname(nickname); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); + if (imageFile != null && !imageFile.isEmpty()) { + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); + if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { + s3Service.deleteExProfile(siteUser); + } + String profileImageUrl = uploadedFile.fileUrl(); + siteUser.setProfileImageUrl(profileImageUrl); + } siteUserRepository.save(siteUser); } @@ -81,12 +82,6 @@ private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { } } - private void validateProfileImageNotEmpty(MultipartFile imageFile) { - if (imageFile == null || imageFile.isEmpty()) { - throw new CustomException(PROFILE_IMAGE_NEEDED); - } - } - private boolean isDefaultProfileImage(String profileImageUrl) { String prefix = "profile/"; return profileImageUrl == null || !profileImageUrl.startsWith(prefix); 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 6e897f04a..334b6f740 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java @@ -29,7 +29,6 @@ import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; 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; @@ -144,18 +143,6 @@ class 프로필_이미지_수정_테스트 { // then then(s3Service).should().deleteExProfile(testUser); } - - @Test - void 빈_이미지_파일로_프로필을_수정하면_예외_응답을_반환한다() { - // given - SiteUser testUser = createSiteUser(); - MockMultipartFile emptyFile = createEmptyImageFile(); - - // when & then - assertThatCode(() -> myPageService.updateMyPageInfo(testUser, emptyFile, "newNickname")) - .isInstanceOf(CustomException.class) - .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); - } } @Nested @@ -273,15 +260,6 @@ private MockMultipartFile createValidImageFile() { ); } - private MockMultipartFile createEmptyImageFile() { - return new MockMultipartFile( - "image", - "empty.jpg", - "image/jpeg", - new byte[0] - ); - } - private String createExpectedErrorMessage(LocalDateTime modifiedAt) { String formatLastModifiedAt = String.format( "(마지막 수정 시간 : %s)", From d322e07f5c2a59fe9d77fe5dc752b694f4b1bd82 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 3 Apr 2025 21:38:01 +0900 Subject: [PATCH 7/8] =?UTF-8?q?chore:=20submodule=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20(#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index a03ce7cf5..42ed07a23 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit a03ce7cf547ab90a660afc4ce202d064ff5a6e3c +Subproject commit 42ed07a23ed2c4947923b57723ca8bfd71b14ca0 From 7c0ab900700d5ad0380c08d72c594df29d9c70de Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 3 Apr 2025 21:38:12 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20codeowners=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EC=96=B4=20=EC=9E=90=EB=8F=99=20=EB=B0=B0=EC=A0=95?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..201ad5570 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Gyuhyeok99 @nayonsoso @wibaek