diff --git a/src/main/generated/com/example/solidconnection/application/domain/QApplication.java b/src/main/generated/com/example/solidconnection/application/domain/QApplication.java index 742603411..764baa7cf 100644 --- a/src/main/generated/com/example/solidconnection/application/domain/QApplication.java +++ b/src/main/generated/com/example/solidconnection/application/domain/QApplication.java @@ -28,6 +28,8 @@ public class QApplication extends EntityPathBase { public final NumberPath id = createNumber("id", Long.class); + public final BooleanPath isDelete = createBoolean("isDelete"); + public final QLanguageTest languageTest; public final StringPath nicknameForApply = createString("nicknameForApply"); @@ -36,6 +38,10 @@ public class QApplication extends EntityPathBase { public final com.example.solidconnection.siteuser.domain.QSiteUser siteUser; + public final StringPath term = createString("term"); + + public final com.example.solidconnection.university.domain.QUniversityInfoForApply thirdChoiceUniversity; + public final NumberPath updateCount = createNumber("updateCount", Integer.class); public final EnumPath verifyStatus = createEnum("verifyStatus", com.example.solidconnection.type.VerifyStatus.class); @@ -63,6 +69,7 @@ public QApplication(Class type, PathMetadata metadata, Pa this.languageTest = inits.isInitialized("languageTest") ? new QLanguageTest(forProperty("languageTest")) : null; this.secondChoiceUniversity = inits.isInitialized("secondChoiceUniversity") ? new com.example.solidconnection.university.domain.QUniversityInfoForApply(forProperty("secondChoiceUniversity"), inits.get("secondChoiceUniversity")) : null; this.siteUser = inits.isInitialized("siteUser") ? new com.example.solidconnection.siteuser.domain.QSiteUser(forProperty("siteUser")) : null; + this.thirdChoiceUniversity = inits.isInitialized("thirdChoiceUniversity") ? new com.example.solidconnection.university.domain.QUniversityInfoForApply(forProperty("thirdChoiceUniversity"), inits.get("thirdChoiceUniversity")) : null; } } diff --git a/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java b/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java index a1879a555..ac4af0986 100644 --- a/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java +++ b/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java @@ -7,6 +7,7 @@ import com.querydsl.core.types.PathMetadata; import javax.annotation.processing.Generated; import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; /** @@ -19,18 +20,32 @@ public class QSiteUser extends EntityPathBase { public static final QSiteUser siteUser = new QSiteUser("siteUser"); + public final EnumPath authType = createEnum("authType", AuthType.class); + public final StringPath birth = createString("birth"); + public final ListPath commentList = this.createList("commentList", com.example.solidconnection.community.comment.domain.Comment.class, com.example.solidconnection.community.comment.domain.QComment.class, PathInits.DIRECT2); + public final StringPath email = createString("email"); public final EnumPath gender = createEnum("gender", com.example.solidconnection.type.Gender.class); + public final ListPath gpaScoreList = this.createList("gpaScoreList", com.example.solidconnection.score.domain.GpaScore.class, com.example.solidconnection.score.domain.QGpaScore.class, PathInits.DIRECT2); + public final NumberPath id = createNumber("id", Long.class); + public final ListPath languageTestScoreList = this.createList("languageTestScoreList", com.example.solidconnection.score.domain.LanguageTestScore.class, com.example.solidconnection.score.domain.QLanguageTestScore.class, PathInits.DIRECT2); + public final StringPath nickname = createString("nickname"); public final DateTimePath nicknameModifiedAt = createDateTime("nicknameModifiedAt", java.time.LocalDateTime.class); + public final StringPath password = createString("password"); + + public final ListPath postLikeList = this.createList("postLikeList", com.example.solidconnection.community.post.domain.PostLike.class, com.example.solidconnection.community.post.domain.QPostLike.class, PathInits.DIRECT2); + + public final ListPath postList = this.createList("postList", com.example.solidconnection.community.post.domain.Post.class, com.example.solidconnection.community.post.domain.QPost.class, PathInits.DIRECT2); + public final EnumPath preparationStage = createEnum("preparationStage", com.example.solidconnection.type.PreparationStatus.class); public final StringPath profileImageUrl = createString("profileImageUrl"); diff --git a/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java b/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java index 5ad64cfd7..4b4c55546 100644 --- a/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java +++ b/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java @@ -40,6 +40,8 @@ public class QUniversityInfoForApply extends EntityPathBase id = createNumber("id", Long.class); + public final StringPath koreanName = createString("koreanName"); + public final SetPath languageRequirements = this.createSet("languageRequirements", LanguageRequirement.class, QLanguageRequirement.class, PathInits.DIRECT2); public final EnumPath semesterAvailableForDispatch = createEnum("semesterAvailableForDispatch", com.example.solidconnection.type.SemesterAvailableForDispatch.class); diff --git a/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java new file mode 100644 index 000000000..ab5bed64b --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.admin.controller; + +import com.example.solidconnection.admin.dto.GpaScoreResponse; +import com.example.solidconnection.admin.dto.GpaScoreSearchResponse; +import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import com.example.solidconnection.admin.service.AdminGpaScoreService; +import com.example.solidconnection.custom.response.PageResponse; +import com.example.solidconnection.util.PagingUtils; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/admin/scores") +@RestController +public class AdminScoreController { + + private final AdminGpaScoreService adminGpaScoreService; + + @GetMapping("/gpas") + public ResponseEntity> searchGpaScores( + @Valid @ModelAttribute ScoreSearchCondition scoreSearchCondition, + @PageableDefault(page = 1) Pageable pageable + ) { + PagingUtils.validatePage(pageable.getPageNumber(), pageable.getPageSize()); + Pageable internalPageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize()); + Page page = adminGpaScoreService.searchGpaScores(scoreSearchCondition, internalPageable); + return ResponseEntity.ok(PageResponse.of(page)); + } + + @PatchMapping("/gpas/{gpa-score-id}") + public ResponseEntity updateGpaScore( + @PathVariable("gpa-score-id") Long gpaScoreId, + @Valid @RequestBody GpaScoreUpdateRequest request + ) { + GpaScoreResponse response = adminGpaScoreService.updateGpaScore(gpaScoreId, request); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java new file mode 100644 index 000000000..564bc724b --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.admin.dto; + +public record GpaResponse( + double gpa, + double gpaCriteria, + String gpaReportUrl +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java new file mode 100644 index 000000000..5f37e823b --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.type.VerifyStatus; + +public record GpaScoreResponse( + long id, + double gpa, + double gpaCriteria, + VerifyStatus verifyStatus, + String rejectedReason +) { + public static GpaScoreResponse from(GpaScore gpaScore) { + return new GpaScoreResponse( + gpaScore.getId(), + gpaScore.getGpa().getGpa(), + gpaScore.getGpa().getGpaCriteria(), + gpaScore.getVerifyStatus(), + gpaScore.getRejectedReason() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java new file mode 100644 index 000000000..2da39fb88 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.admin.dto; + +public record GpaScoreSearchResponse( + GpaScoreStatusResponse gpaScoreStatusResponse, + SiteUserResponse siteUserResponse +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java new file mode 100644 index 000000000..49afbd4ed --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.type.VerifyStatus; + +import java.time.ZonedDateTime; + +public record GpaScoreStatusResponse( + long id, + GpaResponse gpaResponse, + VerifyStatus verifyStatus, + String rejectedReason, + ZonedDateTime createdAt, + ZonedDateTime updatedAt +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java new file mode 100644 index 000000000..bc0b1aa42 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired; +import com.example.solidconnection.type.VerifyStatus; +import jakarta.validation.constraints.NotNull; + +@RejectedReasonRequired +public record GpaScoreUpdateRequest( + + @NotNull(message = "GPA를 입력해주세요.") + Double gpa, + + @NotNull(message = "GPA 기준을 입력해주세요.") + Double gpaCriteria, + + @NotNull(message = "승인 상태를 설정해주세요.") + VerifyStatus verifyStatus, + + String rejectedReason +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java b/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java new file mode 100644 index 000000000..2e94628e6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.type.VerifyStatus; + +import java.time.LocalDate; + +public record ScoreSearchCondition( + VerifyStatus verifyStatus, + String nickname, + LocalDate createdAt) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java b/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java new file mode 100644 index 000000000..1b62f262f --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.admin.dto; + +public record SiteUserResponse( + long id, + String nickname, + String profileImageUrl +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java b/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java new file mode 100644 index 000000000..c761ff485 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.admin.service; + +import com.example.solidconnection.admin.dto.GpaScoreResponse; +import com.example.solidconnection.admin.dto.GpaScoreSearchResponse; +import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.type.VerifyStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.custom.exception.ErrorCode.GPA_SCORE_NOT_FOUND; + +@RequiredArgsConstructor +@Service +public class AdminGpaScoreService { + + private final GpaScoreRepository gpaScoreRepository; + + @Transactional(readOnly = true) + public Page searchGpaScores(ScoreSearchCondition scoreSearchCondition, Pageable pageable) { + return gpaScoreRepository.searchGpaScores(scoreSearchCondition, pageable); + } + + @Transactional + public GpaScoreResponse updateGpaScore(Long gpaScoreId, GpaScoreUpdateRequest request) { + GpaScore gpaScore = gpaScoreRepository.findById(gpaScoreId) + .orElseThrow(() -> new CustomException(GPA_SCORE_NOT_FOUND)); + gpaScore.updateGpaScore( + new Gpa( + request.gpa(), + request.gpaCriteria(), + gpaScore.getGpa().getGpaReportUrl() + ), + request.verifyStatus(), + request.verifyStatus() == VerifyStatus.REJECTED ? request.rejectedReason() : null + ); + return GpaScoreResponse.from(gpaScore); + } +} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index 1a06ec321..5aeb972bf 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -20,13 +20,13 @@ public interface ApplicationRepository extends JpaRepository boolean existsByNicknameForApply(String nicknameForApply); - List findAllByFirstChoiceUniversityAndVerifyStatusAndTerm( + List findAllByFirstChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse( UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllBySecondChoiceUniversityAndVerifyStatusAndTerm( + List findAllBySecondChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse( UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllByThirdChoiceUniversityAndVerifyStatusAndTerm( + List findAllByThirdChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse( UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus, String term); @Query(""" diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 3208d24af..157c6adcd 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -97,7 +97,7 @@ private List getFirstChoiceApplicants(List applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatusAndTerm(uia, VerifyStatus.APPROVED, term) + uia -> applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(uia, VerifyStatus.APPROVED, term) ); } @@ -105,7 +105,7 @@ private List getSecondChoiceApplicants(List applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatusAndTerm(uia, VerifyStatus.APPROVED, term) + uia -> applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(uia, VerifyStatus.APPROVED, term) ); } @@ -113,7 +113,7 @@ private List getThirdChoiceApplicants(List applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatusAndTerm(uia, VerifyStatus.APPROVED, term) + uia -> applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(uia, VerifyStatus.APPROVED, term) ); } 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 ea05c3c0c..dfc455146 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -22,7 +22,7 @@ import java.util.Optional; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE; +import static com.example.solidconnection.custom.exception.ErrorCode.GPA_SCORE_NOT_FOUND; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS; @@ -88,7 +88,7 @@ public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { private GpaScore getValidGpaScore(SiteUser siteUser, Long gpaScoreId) { GpaScore gpaScore = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId) - .orElseThrow(() -> new CustomException(INVALID_GPA_SCORE)); + .orElseThrow(() -> new CustomException(GPA_SCORE_NOT_FOUND)); if (gpaScore.getVerifyStatus() != VerifyStatus.APPROVED) { throw new CustomException(INVALID_GPA_SCORE_STATUS); } 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 dff7b13f0..e99d43f4a 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -40,6 +40,7 @@ public enum ErrorCode { UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "대학교를 찾을 수 없습니다."), REGION_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 지역을 찾을 수 없습니다."), COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."), + GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."), // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), @@ -88,11 +89,15 @@ public enum ErrorCode { NOT_LIKED_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "좋아요하지 않은 대학입니다."), // score - INVALID_GPA_SCORE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 학점입니다."), INVALID_GPA_SCORE_STATUS(HttpStatus.BAD_REQUEST.value(), "학점이 승인되지 않았습니다."), INVALID_LANGUAGE_TEST_SCORE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 어학성적입니다."), INVALID_LANGUAGE_TEST_SCORE_STATUS(HttpStatus.BAD_REQUEST.value(), "어학성적이 승인되지 않았습니다."), USER_DO_NOT_HAVE_GPA(HttpStatus.BAD_REQUEST.value(), "해당 유저의 학점을 찾을 수 없음"), + REJECTED_REASON_REQUIRED(HttpStatus.BAD_REQUEST.value(), "거절 사유가 필요합니다."), + + // page + INVALID_PAGE(HttpStatus.BAD_REQUEST.value(), "페이지 번호는 1 이상 50 이하만 가능합니다."), + INVALID_SIZE(HttpStatus.BAD_REQUEST.value(), "페이지 크기는 1 이상 50 이하만 가능합니다."), // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/custom/response/PageResponse.java b/src/main/java/com/example/solidconnection/custom/response/PageResponse.java new file mode 100644 index 000000000..d1e3479d6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/response/PageResponse.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.custom.response; + +import org.springframework.data.domain.Page; + +import java.util.List; + +public record PageResponse( + List content, + int pageNumber, + int pageSize, + long totalElements, + int totalPages +) { + /* + * 페이지 번호는 1부터 시작하는 것이 사용자 입장에서 더 직관적이기 때문에 1을 더해줌 + */ + public static PageResponse of(Page page) { + return new PageResponse<>( + page.getContent(), + page.getNumber() + 1, + page.getSize(), + page.getTotalElements(), + page.getTotalPages() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/validation/annotation/RejectedReasonRequired.java b/src/main/java/com/example/solidconnection/custom/validation/annotation/RejectedReasonRequired.java new file mode 100644 index 000000000..4ae4a6618 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/validation/annotation/RejectedReasonRequired.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.custom.validation.annotation; + +import com.example.solidconnection.custom.validation.validator.RejectedReasonValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = RejectedReasonValidator.class) +public @interface RejectedReasonRequired { + + String message() default "거절 사유 입력값이 올바르지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java b/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java new file mode 100644 index 000000000..549c36ac5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java @@ -0,0 +1,36 @@ +package com.example.solidconnection.custom.validation.validator; + +import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired; +import com.example.solidconnection.type.VerifyStatus; +import io.micrometer.common.util.StringUtils; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import static com.example.solidconnection.custom.exception.ErrorCode.REJECTED_REASON_REQUIRED; + +public class RejectedReasonValidator implements ConstraintValidator { + + private static final String REJECTED_REASON = "rejectedReason"; + + @Override + public boolean isValid(GpaScoreUpdateRequest request, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + if (isRejectedWithoutReason(request)) { + addValidationError(context, REJECTED_REASON_REQUIRED.getMessage()); + return false; + } + return true; + } + + private boolean isRejectedWithoutReason(GpaScoreUpdateRequest request) { + return request.verifyStatus().equals(VerifyStatus.REJECTED) + && StringUtils.isBlank(request.rejectedReason()); + } + + private void addValidationError(ConstraintValidatorContext context, String message) { + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode(REJECTED_REASON) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java index 54df13759..ddc583aa7 100644 --- a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java @@ -55,4 +55,14 @@ public void setSiteUser(SiteUser siteUser) { this.siteUser = siteUser; siteUser.getGpaScoreList().add(this); } + + public void updateGpaScore(Gpa gpa, VerifyStatus verifyStatus, String rejectedReason) { + this.gpa = gpa; + this.verifyStatus = verifyStatus; + this.rejectedReason = rejectedReason; + } + + public void updateGpa(Gpa gpa) { + this.gpa = gpa; + } } diff --git a/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java b/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java index e3c26665b..5610c8de3 100644 --- a/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java +++ b/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java @@ -1,6 +1,7 @@ package com.example.solidconnection.score.repository; import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.repository.custom.GpaScoreFilterRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,7 +9,7 @@ import java.util.Optional; @Repository -public interface GpaScoreRepository extends JpaRepository { +public interface GpaScoreRepository extends JpaRepository, GpaScoreFilterRepository { Optional findGpaScoreBySiteUser(SiteUser siteUser); diff --git a/src/main/java/com/example/solidconnection/score/repository/custom/GpaScoreFilterRepository.java b/src/main/java/com/example/solidconnection/score/repository/custom/GpaScoreFilterRepository.java new file mode 100644 index 000000000..9429b877a --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/repository/custom/GpaScoreFilterRepository.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.score.repository.custom; + +import com.example.solidconnection.admin.dto.GpaScoreSearchResponse; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface GpaScoreFilterRepository { + + Page searchGpaScores(ScoreSearchCondition scoreSearchCondition, Pageable pageable); +} diff --git a/src/main/java/com/example/solidconnection/score/repository/custom/GpaScoreFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/score/repository/custom/GpaScoreFilterRepositoryImpl.java new file mode 100644 index 000000000..a02e62b49 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/repository/custom/GpaScoreFilterRepositoryImpl.java @@ -0,0 +1,119 @@ +package com.example.solidconnection.score.repository.custom; + +import com.example.solidconnection.admin.dto.GpaResponse; +import com.example.solidconnection.admin.dto.GpaScoreSearchResponse; +import com.example.solidconnection.admin.dto.GpaScoreStatusResponse; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import com.example.solidconnection.admin.dto.SiteUserResponse; +import com.example.solidconnection.type.VerifyStatus; +import com.querydsl.core.types.ConstructorExpression; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; + +import static com.example.solidconnection.score.domain.QGpaScore.gpaScore; +import static com.example.solidconnection.siteuser.domain.QSiteUser.siteUser; +import static org.springframework.util.StringUtils.hasText; + +@Repository +public class GpaScoreFilterRepositoryImpl implements GpaScoreFilterRepository { + + private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault(); + + private static final ConstructorExpression GPA_RESPONSE_PROJECTION = Projections.constructor( + GpaResponse.class, + gpaScore.gpa.gpa, + gpaScore.gpa.gpaCriteria, + gpaScore.gpa.gpaReportUrl + ); + private static final ConstructorExpression GPA_SCORE_STATUS_RESPONSE_PROJECTION = Projections.constructor( + GpaScoreStatusResponse.class, + gpaScore.id, + GPA_RESPONSE_PROJECTION, + gpaScore.verifyStatus, + gpaScore.rejectedReason, + gpaScore.createdAt, + gpaScore.updatedAt + ); + private static final ConstructorExpression SITE_USER_RESPONSE_PROJECTION = Projections.constructor( + SiteUserResponse.class, + siteUser.id, + siteUser.nickname, + siteUser.profileImageUrl + ); + private static final ConstructorExpression GPA_SCORE_SEARCH_RESPONSE_PROJECTION = Projections.constructor( + GpaScoreSearchResponse.class, + GPA_SCORE_STATUS_RESPONSE_PROJECTION, + SITE_USER_RESPONSE_PROJECTION + ); + + private final JPAQueryFactory queryFactory; + + @Autowired + public GpaScoreFilterRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page searchGpaScores(ScoreSearchCondition condition, Pageable pageable) { + List content = queryFactory + .select(GPA_SCORE_SEARCH_RESPONSE_PROJECTION) + .from(gpaScore) + .join(gpaScore.siteUser, siteUser) + .where( + verifyStatusEq(condition.verifyStatus()), + nicknameContains(condition.nickname()), + createdAtEq(condition.createdAt()) + ) + .orderBy(gpaScore.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long totalCount = queryFactory + .select(gpaScore.count()) + .from(gpaScore) + .join(gpaScore.siteUser, siteUser) + .where( + verifyStatusEq(condition.verifyStatus()), + nicknameContains(condition.nickname()), + createdAtEq(condition.createdAt()) + ) + .fetchOne(); + + return new PageImpl<>(content, pageable, totalCount != null ? totalCount : 0L); + } + + private BooleanExpression verifyStatusEq(VerifyStatus verifyStatus) { + return verifyStatus != null ? gpaScore.verifyStatus.eq(verifyStatus) : null; + } + + private BooleanExpression nicknameContains(String nickname) { + return hasText(nickname) ? siteUser.nickname.contains(nickname) : null; + } + + private BooleanExpression createdAtEq(LocalDate createdAt) { + if (createdAt == null) { + return null; + } + + LocalDateTime startOfDay = createdAt.atStartOfDay(); + LocalDateTime endOfDay = createdAt.plusDays(1).atStartOfDay().minusNanos(1); + + return gpaScore.createdAt.between( + startOfDay.atZone(SYSTEM_ZONE_ID), + endOfDay.atZone(SYSTEM_ZONE_ID) + ); + } +} 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 2f43337ed..46f1555f1 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.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 ) { siteUserService.updateMyPageInfo(siteUser, imageFile, nickname); return ResponseEntity.ok().build(); 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 30297283a..c2ee4ded7 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -48,19 +48,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 +83,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/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index 30249dc82..29082c98e 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -15,7 +15,9 @@ public enum LanguageTestType { TEF(LanguageTestType::compareIntegerScores), TOEFL_IBT(LanguageTestType::compareIntegerScores), TOEFL_ITP(LanguageTestType::compareIntegerScores), - TOEIC(LanguageTestType::compareIntegerScores); + TOEIC(LanguageTestType::compareIntegerScores), + ETC((s1, s2) -> 0), // 기타 언어시험은 점수를 비교할 수 없으므로 항상 크다고 비교한다. + ; private final Comparator comparator; diff --git a/src/main/java/com/example/solidconnection/util/PagingUtils.java b/src/main/java/com/example/solidconnection/util/PagingUtils.java new file mode 100644 index 000000000..5b4547410 --- /dev/null +++ b/src/main/java/com/example/solidconnection/util/PagingUtils.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_PAGE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SIZE; + +public class PagingUtils { + + private static final int MIN_PAGE = 1; + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 50; + private static final int MAX_PAGE = 50; + + private PagingUtils() { + } + + public static void validatePage(int page, int size) { + if (page < MIN_PAGE || page > MAX_PAGE) { + throw new CustomException(INVALID_PAGE); + } + if (size < MIN_SIZE || size > MAX_SIZE) { + throw new CustomException(INVALID_SIZE); + } + } +} diff --git a/src/test/java/com/example/solidconnection/admin/service/GpaScoreVerificationAdminServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/GpaScoreVerificationAdminServiceTest.java new file mode 100644 index 000000000..abfd226de --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/service/GpaScoreVerificationAdminServiceTest.java @@ -0,0 +1,234 @@ +package com.example.solidconnection.admin.service; + +import com.example.solidconnection.admin.dto.GpaScoreResponse; +import com.example.solidconnection.admin.dto.GpaScoreSearchResponse; +import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +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.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.type.VerifyStatus; +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.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDate; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.GPA_SCORE_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("학점 검증 관리자 서비스 테스트") +class GpaScoreVerificationAdminServiceTest extends BaseIntegrationTest { + + @Autowired + private AdminGpaScoreService adminGpaScoreService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + private SiteUser siteUser1; + private SiteUser siteUser2; + private SiteUser siteUser3; + private GpaScore gpaScore1; + private GpaScore gpaScore2; + private GpaScore gpaScore3; + + @BeforeEach + void setUp() { + siteUser1 = createSiteUser(1, "test1"); + siteUser2 = createSiteUser(2, "test2"); + siteUser3 = createSiteUser(3, "test3"); + gpaScore3 = createGpaScore(siteUser3, VerifyStatus.REJECTED); + gpaScore2 = createGpaScore(siteUser2, VerifyStatus.PENDING); + gpaScore1 = createGpaScore(siteUser1, VerifyStatus.PENDING); + } + + @Nested + class 지원한_GPA_목록_조회 { + + @Test + void 검증_상태를_조건으로_페이징하여_조회한다() { + // given + ScoreSearchCondition condition = new ScoreSearchCondition(VerifyStatus.PENDING, null, null); + Pageable pageable = PageRequest.of(0, 10); + List expectedGpaScores = List.of(gpaScore1, gpaScore2); + + // when + Page response = adminGpaScoreService.searchGpaScores(condition, pageable); + + // then + assertThat(response.getContent()) + .hasSize(expectedGpaScores.size()) + .zipSatisfy(expectedGpaScores, (actual, expected) -> assertAll( + () -> assertThat(actual.gpaScoreStatusResponse().id()).isEqualTo(expected.getId()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpa()).isEqualTo(expected.getGpa().getGpa()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpaCriteria()).isEqualTo(expected.getGpa().getGpaCriteria()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpaReportUrl()).isEqualTo(expected.getGpa().getGpaReportUrl()), + () -> assertThat(actual.gpaScoreStatusResponse().verifyStatus()).isEqualTo(expected.getVerifyStatus()), + + () -> assertThat(actual.siteUserResponse().id()).isEqualTo(expected.getSiteUser().getId()), + () -> assertThat(actual.siteUserResponse().profileImageUrl()).isEqualTo(expected.getSiteUser().getProfileImageUrl()), + () -> assertThat(actual.siteUserResponse().nickname()).isEqualTo(expected.getSiteUser().getNickname()) + )); + } + + @Test + void 닉네임으로_페이징하여_조회한다() { + // given + ScoreSearchCondition condition = new ScoreSearchCondition(null, "test", null); + Pageable pageable = PageRequest.of(0, 10); + List expectedGpaScores = List.of(gpaScore1, gpaScore2, gpaScore3); + + // when + Page response = adminGpaScoreService.searchGpaScores(condition, pageable); + + // then + assertThat(response.getContent()) + .hasSize(expectedGpaScores.size()) + .zipSatisfy(expectedGpaScores, (actual, expected) -> assertAll( + () -> assertThat(actual.gpaScoreStatusResponse().id()).isEqualTo(expected.getId()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpa()).isEqualTo(expected.getGpa().getGpa()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpaCriteria()).isEqualTo(expected.getGpa().getGpaCriteria()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpaReportUrl()).isEqualTo(expected.getGpa().getGpaReportUrl()), + () -> assertThat(actual.gpaScoreStatusResponse().verifyStatus()).isEqualTo(expected.getVerifyStatus()), + + () -> assertThat(actual.siteUserResponse().id()).isEqualTo(expected.getSiteUser().getId()), + () -> assertThat(actual.siteUserResponse().profileImageUrl()).isEqualTo(expected.getSiteUser().getProfileImageUrl()), + () -> assertThat(actual.siteUserResponse().nickname()).isEqualTo(expected.getSiteUser().getNickname()) + )); + } + + @Test + void 모든_조건으로_페이징하여_조회한다() { + // given + ScoreSearchCondition condition = new ScoreSearchCondition(VerifyStatus.PENDING, "test1", LocalDate.now()); + Pageable pageable = PageRequest.of(0, 10); + List expectedGpaScores = List.of(gpaScore1); + + // when + Page response = adminGpaScoreService.searchGpaScores(condition, pageable); + + // then + assertThat(response.getContent()) + .hasSize(expectedGpaScores.size()) + .zipSatisfy(expectedGpaScores, (actual, expected) -> assertAll( + () -> assertThat(actual.gpaScoreStatusResponse().id()).isEqualTo(expected.getId()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpa()).isEqualTo(expected.getGpa().getGpa()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpaCriteria()).isEqualTo(expected.getGpa().getGpaCriteria()), + () -> assertThat(actual.gpaScoreStatusResponse().gpaResponse().gpaReportUrl()).isEqualTo(expected.getGpa().getGpaReportUrl()), + () -> assertThat(actual.gpaScoreStatusResponse().verifyStatus()).isEqualTo(expected.getVerifyStatus()), + + () -> assertThat(actual.siteUserResponse().id()).isEqualTo(expected.getSiteUser().getId()), + () -> assertThat(actual.siteUserResponse().profileImageUrl()).isEqualTo(expected.getSiteUser().getProfileImageUrl()), + () -> assertThat(actual.siteUserResponse().nickname()).isEqualTo(expected.getSiteUser().getNickname()) + )); + } + } + + @Nested + class GPA_점수_검증_및_수정 { + + @Test + void GPA와_검증상태를_정상적으로_수정한다() { + // given + GpaScoreUpdateRequest request = new GpaScoreUpdateRequest( + 3.8, + 4.3, + VerifyStatus.APPROVED, + null + ); + + // when + GpaScoreResponse response = adminGpaScoreService.updateGpaScore(gpaScore1.getId(), request); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(gpaScore1.getId()), + () -> assertThat(response.gpa()).isEqualTo(request.gpa()), + () -> assertThat(response.gpaCriteria()).isEqualTo(request.gpaCriteria()), + () -> assertThat(response.verifyStatus()).isEqualTo(request.verifyStatus()), + () -> assertThat(response.rejectedReason()).isNull() + ); + } + + @Test + void 승인상태로_변경_시_거절사유가_입력되어도_null로_저장된다() { + // given + GpaScoreUpdateRequest request = new GpaScoreUpdateRequest( + 3.8, + 4.3, + VerifyStatus.APPROVED, + "이 거절사유는 무시되어야 함" + ); + + // when + GpaScoreResponse response = adminGpaScoreService.updateGpaScore(gpaScore1.getId(), request); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(gpaScore1.getId()), + () -> assertThat(response.gpa()).isEqualTo(request.gpa()), + () -> assertThat(response.gpaCriteria()).isEqualTo(request.gpaCriteria()), + () -> assertThat(response.verifyStatus()).isEqualTo(VerifyStatus.APPROVED), + () -> assertThat(response.rejectedReason()).isNull() + ); + } + + @Test + void 존재하지_않는_GPA_수정_시_예외_응답을_반환한다() { + // given + long invalidGpaScoreId = 9999L; + GpaScoreUpdateRequest request = new GpaScoreUpdateRequest( + 3.8, + 4.3, + VerifyStatus.APPROVED, + null + ); + + // when & then + assertThatCode(() -> adminGpaScoreService.updateGpaScore(invalidGpaScoreId, request)) + .isInstanceOf(CustomException.class) + .hasMessage(GPA_SCORE_NOT_FOUND.getMessage()); + } + } + + private SiteUser createSiteUser(int index, String nickname) { + SiteUser siteUser = new SiteUser( + "test" + index + " @example.com", + nickname, + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private GpaScore createGpaScore(SiteUser siteUser, VerifyStatus status) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser + ); + gpaScore.setVerifyStatus(status); + return gpaScoreRepository.save(gpaScore); + } +} diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java index f06116ebb..583c31d80 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java @@ -1,9 +1,21 @@ package com.example.solidconnection.application.service; +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.ApplicantResponse; import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; +import com.example.solidconnection.university.domain.UniversityInfoForApply; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -19,6 +31,15 @@ class ApplicationQueryServiceTest extends BaseIntegrationTest { @Autowired private ApplicationQueryService applicationQueryService; + @Autowired + private ApplicationRepository applicationRepository; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + @Nested class 지원자_목록_조회_테스트 { @@ -143,6 +164,26 @@ class 지원자_목록_조회_테스트 { List.of(ApplicantResponse.of(이전학기_지원서, false))) )); } + + @Test + void 동일_유저의_여러_지원서_중_최신_지원서만_조회된다() { + // given + Application firstApplication = createApplication(테스트유저_1, 괌대학_A_지원_정보); + firstApplication.setIsDeleteTrue(); + applicationRepository.save(firstApplication); + Application secondApplication = createApplication(테스트유저_1, 네바다주립대학_라스베이거스_지원_정보); + + + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_1, "", ""); + + // then + assertThat(response.firstChoice().stream() + .flatMap(univ -> univ.applicants().stream()) + .filter(ApplicantResponse::isMine)) + .containsExactly(ApplicantResponse.of(secondApplication, true)); + } } @Nested @@ -211,4 +252,39 @@ class 경쟁자_목록_조회_테스트 { assertThat(response.thirdChoice()).isEmpty(); } } + + private GpaScore createApprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser + ); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + siteUser + ); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + return languageTestScoreRepository.save(languageTestScore); + } + + private Application createApplication( + SiteUser siteUser, + UniversityInfoForApply universityInfoForApply) { + Application application = new Application( + siteUser, + createApprovedGpaScore(siteUser).getGpa(), + createApprovedLanguageTestScore(siteUser).getLanguageTest(), + term, + universityInfoForApply, + null, + null, + null + ); + application.setVerifyStatus(VerifyStatus.APPROVED); + return applicationRepository.save(application); + } } diff --git a/src/test/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidatorTest.java b/src/test/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidatorTest.java new file mode 100644 index 000000000..ecadd8aff --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidatorTest.java @@ -0,0 +1,66 @@ +package com.example.solidconnection.custom.validation.validator; + +import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.type.VerifyStatus; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static com.example.solidconnection.custom.exception.ErrorCode.REJECTED_REASON_REQUIRED; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("거절 사유 유효성 검사 테스트") +class RejectedReasonValidatorTest { + + private static final String MESSAGE = "message"; + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void 거절_상태일_때_거절사유가_있으면_유효하다() { + // given + GpaScoreUpdateRequest request = new GpaScoreUpdateRequest( + 3.0, + 4.5, + VerifyStatus.REJECTED, + "부적합" + ); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 거절_상태일_때_거절사유가_없으면_예외_응답을_반환한다() { + // given + GpaScoreUpdateRequest request = new GpaScoreUpdateRequest( + 3.0, + 4.5, + VerifyStatus.REJECTED, + null + ); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .extracting(MESSAGE) + .contains(REJECTED_REASON_REQUIRED.getMessage()); + } +} 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 c6236aedf..819f33156 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.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.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; @@ -145,18 +144,6 @@ class 프로필_이미지_수정_테스트 { // then then(s3Service).should().deleteExProfile(testUser); } - - @Test - void 빈_이미지_파일로_프로필을_수정하면_예외_응답을_반환한다() { - // given - SiteUser testUser = createSiteUser(); - MockMultipartFile emptyFile = createEmptyImageFile(); - - // when & then - assertThatCode(() -> siteUserService.updateMyPageInfo(testUser, emptyFile, "newNickname")) - .isInstanceOf(CustomException.class) - .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); - } } @Nested @@ -274,15 +261,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)", diff --git a/src/test/java/com/example/solidconnection/util/PagingUtilsTest.java b/src/test/java/com/example/solidconnection/util/PagingUtilsTest.java new file mode 100644 index 000000000..f8a10a473 --- /dev/null +++ b/src/test/java/com/example/solidconnection/util/PagingUtilsTest.java @@ -0,0 +1,61 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_PAGE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SIZE; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +@DisplayName("PagingUtils 테스트") +class PagingUtilsTest { + + private static final int VALID_PAGE = 1; + private static final int VALID_SIZE = 10; + + private static final int MIN_PAGE = 1; + private static final int MAX_PAGE = 50; + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 50; + + @Test + @DisplayName("유효한 페이지 번호와 크기가 주어지면 예외가 발생하지 않는다") + void validateValidPageAndSize() { + // when & then + assertThatCode(() -> PagingUtils.validatePage(VALID_PAGE, VALID_SIZE)) + .doesNotThrowAnyException(); + } + + @Test + void 최소_페이지_번호보다_작으면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> PagingUtils.validatePage(MIN_PAGE - 1, VALID_SIZE)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_PAGE.getMessage()); + } + + @Test + void 최대_페이지_번호보다_크면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> PagingUtils.validatePage(MAX_PAGE + 1, VALID_SIZE)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_PAGE.getMessage()); + } + + @Test + void 최소_페이지_크기보다_작으면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> PagingUtils.validatePage(VALID_PAGE, MIN_SIZE - 1)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_SIZE.getMessage()); + } + + @Test + void 최대_페이지_크기보다_크면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> PagingUtils.validatePage(VALID_PAGE, MAX_SIZE + 1)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_SIZE.getMessage()); + } +}