diff --git a/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java index ab5bed64b..ca0692936 100644 --- a/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java +++ b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java @@ -3,8 +3,12 @@ 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.LanguageTestScoreResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreSearchResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreUpdateRequest; import com.example.solidconnection.admin.dto.ScoreSearchCondition; import com.example.solidconnection.admin.service.AdminGpaScoreService; +import com.example.solidconnection.admin.service.AdminLanguageTestScoreService; import com.example.solidconnection.custom.response.PageResponse; import com.example.solidconnection.util.PagingUtils; import jakarta.validation.Valid; @@ -18,6 +22,7 @@ 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -28,7 +33,9 @@ public class AdminScoreController { private final AdminGpaScoreService adminGpaScoreService; + private final AdminLanguageTestScoreService adminLanguageTestScoreService; + // todo: 추후 커스텀 페이지 객체 & argumentResolver를 적용 필요 @GetMapping("/gpas") public ResponseEntity> searchGpaScores( @Valid @ModelAttribute ScoreSearchCondition scoreSearchCondition, @@ -40,7 +47,7 @@ public ResponseEntity> searchGpaScores( return ResponseEntity.ok(PageResponse.of(page)); } - @PatchMapping("/gpas/{gpa-score-id}") + @PutMapping("/gpas/{gpa-score-id}") public ResponseEntity updateGpaScore( @PathVariable("gpa-score-id") Long gpaScoreId, @Valid @RequestBody GpaScoreUpdateRequest request @@ -48,4 +55,25 @@ public ResponseEntity updateGpaScore( GpaScoreResponse response = adminGpaScoreService.updateGpaScore(gpaScoreId, request); return ResponseEntity.ok(response); } + + // todo: 추후 커스텀 페이지 객체 & argumentResolver를 적용 필요 + @GetMapping("/language-tests") + public ResponseEntity> searchLanguageTestScores( + @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 = adminLanguageTestScoreService.searchLanguageTestScores(scoreSearchCondition, internalPageable); + return ResponseEntity.ok(PageResponse.of(page)); + } + + @PutMapping("/language-tests/{language-test-score-id}") + public ResponseEntity updateLanguageTestScore( + @PathVariable("language-test-score-id") Long languageTestScoreId, + @Valid @RequestBody LanguageTestScoreUpdateRequest request + ) { + LanguageTestScoreResponse response = adminLanguageTestScoreService.updateLanguageTestScore(languageTestScoreId, request); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java index bc0b1aa42..b7fe58c71 100644 --- a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java @@ -17,5 +17,5 @@ public record GpaScoreUpdateRequest( VerifyStatus verifyStatus, String rejectedReason -) { +) implements ScoreUpdateRequest { } diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java new file mode 100644 index 000000000..c91fc68c3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.type.LanguageTestType; + +public record LanguageTestResponse( + LanguageTestType languageTestType, + String languageTestScore, + String languageTestReportUrl +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java new file mode 100644 index 000000000..aee435c9c --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; + +public record LanguageTestScoreResponse( + long id, + LanguageTestType languageTestType, + String languageTestScore, + VerifyStatus verifyStatus, + String rejectedReason +) { + public static LanguageTestScoreResponse from(LanguageTestScore languageTestScore) { + return new LanguageTestScoreResponse( + languageTestScore.getId(), + languageTestScore.getLanguageTest().getLanguageTestType(), + languageTestScore.getLanguageTest().getLanguageTestScore(), + languageTestScore.getVerifyStatus(), + languageTestScore.getRejectedReason() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java new file mode 100644 index 000000000..0e1830f66 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.admin.dto; + +public record LanguageTestScoreSearchResponse( + LanguageTestScoreStatusResponse languageTestScoreStatusResponse, + SiteUserResponse siteUserResponse +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java new file mode 100644 index 000000000..c852b5b2a --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.type.VerifyStatus; + +import java.time.ZonedDateTime; + +public record LanguageTestScoreStatusResponse( + long id, + LanguageTestResponse languageTestResponse, + VerifyStatus verifyStatus, + String rejectedReason, + ZonedDateTime createdAt, + ZonedDateTime updatedAt +) { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java new file mode 100644 index 000000000..3e76e0c93 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; +import jakarta.validation.constraints.NotNull; + +@RejectedReasonRequired +public record LanguageTestScoreUpdateRequest( + + @NotNull(message = "어학 유형을 입력해주세요.") + LanguageTestType languageTestType, + + @NotNull(message = "어학 점수를 입력해주세요.") + String languageTestScore, + + @NotNull(message = "승인 상태를 설정해주세요.") + VerifyStatus verifyStatus, + + String rejectedReason +) implements ScoreUpdateRequest { +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java new file mode 100644 index 000000000..184f76100 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.type.VerifyStatus; + +public interface ScoreUpdateRequest { + VerifyStatus verifyStatus(); + String rejectedReason(); +} diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java b/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java new file mode 100644 index 000000000..380ef02c6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.admin.service; + +import com.example.solidconnection.admin.dto.LanguageTestScoreResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreSearchResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreUpdateRequest; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +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.LANGUAGE_TEST_SCORE_NOT_FOUND; + +@RequiredArgsConstructor +@Service +public class AdminLanguageTestScoreService { + + private final LanguageTestScoreRepository languageTestScoreRepository; + + @Transactional(readOnly = true) + public Page searchLanguageTestScores(ScoreSearchCondition scoreSearchCondition, Pageable pageable) { + return languageTestScoreRepository.searchLanguageTestScores(scoreSearchCondition, pageable); + } + + @Transactional + public LanguageTestScoreResponse updateLanguageTestScore(Long languageTestScoreId, LanguageTestScoreUpdateRequest request) { + LanguageTestScore languageTestScore = languageTestScoreRepository.findById(languageTestScoreId) + .orElseThrow(() -> new CustomException(LANGUAGE_TEST_SCORE_NOT_FOUND)); + languageTestScore.updateLanguageTestScore( + new LanguageTest( + request.languageTestType(), + request.languageTestScore(), + languageTestScore.getLanguageTest().getLanguageTestReportUrl() + ), + request.verifyStatus(), + request.verifyStatus() == VerifyStatus.REJECTED ? request.rejectedReason() : null + ); + return LanguageTestScoreResponse.from(languageTestScore); + } +} 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 e99d43f4a..52bab0980 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -41,6 +41,7 @@ public enum ErrorCode { 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(), "존재하지 않는 학점입니다."), + LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."), // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), 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 index 549c36ac5..c975a0a0a 100644 --- a/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java +++ b/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java @@ -1,6 +1,6 @@ package com.example.solidconnection.custom.validation.validator; -import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.admin.dto.ScoreUpdateRequest; import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired; import com.example.solidconnection.type.VerifyStatus; import io.micrometer.common.util.StringUtils; @@ -9,12 +9,12 @@ import static com.example.solidconnection.custom.exception.ErrorCode.REJECTED_REASON_REQUIRED; -public class RejectedReasonValidator implements ConstraintValidator { +public class RejectedReasonValidator implements ConstraintValidator { private static final String REJECTED_REASON = "rejectedReason"; @Override - public boolean isValid(GpaScoreUpdateRequest request, ConstraintValidatorContext context) { + public boolean isValid(ScoreUpdateRequest request, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); if (isRejectedWithoutReason(request)) { addValidationError(context, REJECTED_REASON_REQUIRED.getMessage()); @@ -23,7 +23,7 @@ public boolean isValid(GpaScoreUpdateRequest request, ConstraintValidatorContext return true; } - private boolean isRejectedWithoutReason(GpaScoreUpdateRequest request) { + private boolean isRejectedWithoutReason(ScoreUpdateRequest request) { return request.verifyStatus().equals(VerifyStatus.REJECTED) && StringUtils.isBlank(request.rejectedReason()); } diff --git a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java index 7939e1db8..ec791373e 100644 --- a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java @@ -54,4 +54,10 @@ public void setSiteUser(SiteUser siteUser) { this.siteUser = siteUser; siteUser.getLanguageTestScoreList().add(this); } + + public void updateLanguageTestScore(LanguageTest languageTest, VerifyStatus verifyStatus, String rejectedReason) { + this.languageTest = languageTest; + this.verifyStatus = verifyStatus; + this.rejectedReason = rejectedReason; + } } diff --git a/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java b/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java index 8e6ca3967..5bef377cf 100644 --- a/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java +++ b/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java @@ -1,6 +1,7 @@ package com.example.solidconnection.score.repository; import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.custom.LanguageTestScoreFilterRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.LanguageTestType; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,7 +10,7 @@ import java.util.Optional; @Repository -public interface LanguageTestScoreRepository extends JpaRepository { +public interface LanguageTestScoreRepository extends JpaRepository, LanguageTestScoreFilterRepository { Optional findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(SiteUser siteUser, LanguageTestType languageTestType); diff --git a/src/main/java/com/example/solidconnection/score/repository/custom/LanguageTestScoreFilterRepository.java b/src/main/java/com/example/solidconnection/score/repository/custom/LanguageTestScoreFilterRepository.java new file mode 100644 index 000000000..d1d8830fa --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/repository/custom/LanguageTestScoreFilterRepository.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.score.repository.custom; + +import com.example.solidconnection.admin.dto.LanguageTestScoreSearchResponse; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface LanguageTestScoreFilterRepository { + + Page searchLanguageTestScores(ScoreSearchCondition scoreSearchCondition, Pageable pageable); +} diff --git a/src/main/java/com/example/solidconnection/score/repository/custom/LanguageTestScoreFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/score/repository/custom/LanguageTestScoreFilterRepositoryImpl.java new file mode 100644 index 000000000..5d88c1451 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/repository/custom/LanguageTestScoreFilterRepositoryImpl.java @@ -0,0 +1,119 @@ +package com.example.solidconnection.score.repository.custom; + +import com.example.solidconnection.admin.dto.LanguageTestResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreSearchResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreStatusResponse; +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.QLanguageTestScore.languageTestScore; +import static com.example.solidconnection.siteuser.domain.QSiteUser.siteUser; +import static io.jsonwebtoken.lang.Strings.hasText; + +@Repository +public class LanguageTestScoreFilterRepositoryImpl implements LanguageTestScoreFilterRepository { + + private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault(); + + private static final ConstructorExpression LANGUAGE_TEST_RESPONSE_PROJECTION = Projections.constructor( + LanguageTestResponse.class, + languageTestScore.languageTest.languageTestType, + languageTestScore.languageTest.languageTestScore, + languageTestScore.languageTest.languageTestReportUrl + ); + private static final ConstructorExpression LANGUAGE_TEST_SCORE_STATUS_RESPONSE_PROJECTION = Projections.constructor( + LanguageTestScoreStatusResponse.class, + languageTestScore.id, + LANGUAGE_TEST_RESPONSE_PROJECTION, + languageTestScore.verifyStatus, + languageTestScore.rejectedReason, + languageTestScore.createdAt, + languageTestScore.updatedAt + ); + private static final ConstructorExpression SITE_USER_RESPONSE_PROJECTION = Projections.constructor( + SiteUserResponse.class, + siteUser.id, + siteUser.nickname, + siteUser.profileImageUrl + ); + private static final ConstructorExpression LANGUAGE_TEST_SCORE_SEARCH_RESPONSE_PROJECTION = Projections.constructor( + LanguageTestScoreSearchResponse.class, + LANGUAGE_TEST_SCORE_STATUS_RESPONSE_PROJECTION, + SITE_USER_RESPONSE_PROJECTION + ); + + private final JPAQueryFactory queryFactory; + + @Autowired + public LanguageTestScoreFilterRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page searchLanguageTestScores(ScoreSearchCondition condition, Pageable pageable) { + List content = queryFactory + .select(LANGUAGE_TEST_SCORE_SEARCH_RESPONSE_PROJECTION) + .from(languageTestScore) + .join(languageTestScore.siteUser, siteUser) + .where( + verifyStatusEq(condition.verifyStatus()), + nicknameContains(condition.nickname()), + createdAtEq(condition.createdAt()) + ) + .orderBy(languageTestScore.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long totalCount = queryFactory + .select(languageTestScore.count()) + .from(languageTestScore) + .join(languageTestScore.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 ? languageTestScore.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 languageTestScore.createdAt.between( + startOfDay.atZone(SYSTEM_ZONE_ID), + endOfDay.atZone(SYSTEM_ZONE_ID) + ); + } +} diff --git a/src/main/resources/secret b/src/main/resources/secret index be8cb3fdc..8cc138ff7 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit be8cb3fdc04015fee0eb46e97c917b5e048bca8a +Subproject commit 8cc138ff74606572dbf86f086076038087bf3497 diff --git a/src/test/java/com/example/solidconnection/admin/service/GpaScoreVerificationAdminServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/admin/service/GpaScoreVerificationAdminServiceTest.java rename to src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java index abfd226de..a7d4e91e9 100644 --- a/src/test/java/com/example/solidconnection/admin/service/GpaScoreVerificationAdminServiceTest.java +++ b/src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java @@ -33,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("학점 검증 관리자 서비스 테스트") -class GpaScoreVerificationAdminServiceTest extends BaseIntegrationTest { +class AdminGpaScoreServiceTest extends BaseIntegrationTest { @Autowired private AdminGpaScoreService adminGpaScoreService; diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreServiceTest.java new file mode 100644 index 000000000..8882108b9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreServiceTest.java @@ -0,0 +1,244 @@ +package com.example.solidconnection.admin.service; + +import com.example.solidconnection.admin.dto.LanguageTestScoreResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreSearchResponse; +import com.example.solidconnection.admin.dto.LanguageTestScoreUpdateRequest; +import com.example.solidconnection.admin.dto.ScoreSearchCondition; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +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.LANGUAGE_TEST_SCORE_NOT_FOUND; +import static com.example.solidconnection.type.LanguageTestType.TOEIC; +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 AdminLanguageTestScoreServiceTest extends BaseIntegrationTest { + + @Autowired + private AdminLanguageTestScoreService adminLanguageTestScoreService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + + private SiteUser siteUser1; + private SiteUser siteUser2; + private SiteUser siteUser3; + private LanguageTestScore languageTestScore1; + private LanguageTestScore languageTestScore2; + private LanguageTestScore languageTestScore3; + + @BeforeEach + void setUp() { + siteUser1 = createSiteUser(1, "test1"); + siteUser2 = createSiteUser(2, "test2"); + siteUser3 = createSiteUser(3, "test3"); + languageTestScore3 = createLanguageTestScore(siteUser3, VerifyStatus.REJECTED); + languageTestScore2 = createLanguageTestScore(siteUser2, VerifyStatus.PENDING); + languageTestScore1 = createLanguageTestScore(siteUser1, VerifyStatus.PENDING); + } + + @Nested + class 지원한_어학_목록_조회 { + + @Test + void 검증_상태를_조건으로_페이징하여_조회한다() { + // given + ScoreSearchCondition condition = new ScoreSearchCondition(VerifyStatus.PENDING, null, null); + Pageable pageable = PageRequest.of(0, 10); + List expectedLanguageTestScores = List.of(languageTestScore1, languageTestScore2); + + // when + Page response = adminLanguageTestScoreService.searchLanguageTestScores(condition, pageable); + + // then + assertThat(response.getContent()) + .hasSize(expectedLanguageTestScores.size()) + .zipSatisfy(expectedLanguageTestScores, (actual, expected) -> assertAll( + () -> assertThat(actual.languageTestScoreStatusResponse().id()).isEqualTo(expected.getId()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestType()) + .isEqualTo(expected.getLanguageTest().getLanguageTestType()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestScore()) + .isEqualTo(expected.getLanguageTest().getLanguageTestScore()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestReportUrl()) + .isEqualTo(expected.getLanguageTest().getLanguageTestReportUrl()), + () -> assertThat(actual.languageTestScoreStatusResponse().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 expectedLanguageTestScores = List.of(languageTestScore1, languageTestScore2, languageTestScore3); + + // when + Page response = adminLanguageTestScoreService.searchLanguageTestScores(condition, pageable); + + // then + assertThat(response.getContent()) + .hasSize(expectedLanguageTestScores.size()) + .zipSatisfy(expectedLanguageTestScores, (actual, expected) -> assertAll( + () -> assertThat(actual.languageTestScoreStatusResponse().id()).isEqualTo(expected.getId()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestType()) + .isEqualTo(expected.getLanguageTest().getLanguageTestType()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestScore()) + .isEqualTo(expected.getLanguageTest().getLanguageTestScore()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestReportUrl()) + .isEqualTo(expected.getLanguageTest().getLanguageTestReportUrl()), + () -> assertThat(actual.languageTestScoreStatusResponse().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 expectedLanguageTestScores = List.of(languageTestScore1); + + // when + Page response = adminLanguageTestScoreService.searchLanguageTestScores(condition, pageable); + + // then + assertThat(response.getContent()) + .hasSize(expectedLanguageTestScores.size()) + .zipSatisfy(expectedLanguageTestScores, (actual, expected) -> assertAll( + () -> assertThat(actual.languageTestScoreStatusResponse().id()).isEqualTo(expected.getId()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestType()) + .isEqualTo(expected.getLanguageTest().getLanguageTestType()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestScore()) + .isEqualTo(expected.getLanguageTest().getLanguageTestScore()), + () -> assertThat(actual.languageTestScoreStatusResponse().languageTestResponse().languageTestReportUrl()) + .isEqualTo(expected.getLanguageTest().getLanguageTestReportUrl()), + () -> assertThat(actual.languageTestScoreStatusResponse().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 어학점수_검증_및_수정 { + + @Test + void 어학점수와_검증상태를_정상적으로_수정한다() { + // given + LanguageTestScoreUpdateRequest request = new LanguageTestScoreUpdateRequest( + TOEIC, + "850", + VerifyStatus.APPROVED, + null + ); + + // when + LanguageTestScoreResponse response = adminLanguageTestScoreService.updateLanguageTestScore(languageTestScore1.getId(), request); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(languageTestScore1.getId()), + () -> assertThat(response.languageTestType()).isEqualTo(request.languageTestType()), + () -> assertThat(response.languageTestScore()).isEqualTo(request.languageTestScore()), + () -> assertThat(response.verifyStatus()).isEqualTo(request.verifyStatus()), + () -> assertThat(response.rejectedReason()).isNull() + ); + } + + @Test + void 승인상태로_변경_시_거절사유가_입력되어도_null로_저장된다() { + // given + LanguageTestScoreUpdateRequest request = new LanguageTestScoreUpdateRequest( + TOEIC, + "850", + VerifyStatus.APPROVED, + "이 거절사유는 무시되어야 함" + ); + + // when + LanguageTestScoreResponse response = adminLanguageTestScoreService.updateLanguageTestScore(languageTestScore1.getId(), request); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(languageTestScore1.getId()), + () -> assertThat(response.languageTestType()).isEqualTo(request.languageTestType()), + () -> assertThat(response.languageTestScore()).isEqualTo(request.languageTestScore()), + () -> assertThat(response.verifyStatus()).isEqualTo(VerifyStatus.APPROVED), + () -> assertThat(response.rejectedReason()).isNull() + ); + } + + @Test + void 존재하지_않는_어학점수_수정_시_예외_응답을_반환한다() { + // given + long invalidLanguageTestScoreId = 9999L; + LanguageTestScoreUpdateRequest request = new LanguageTestScoreUpdateRequest( + TOEIC, + "850", + VerifyStatus.APPROVED, + null + ); + + // when & then + assertThatCode(() -> adminLanguageTestScoreService.updateLanguageTestScore(invalidLanguageTestScoreId, request)) + .isInstanceOf(CustomException.class) + .hasMessage(LANGUAGE_TEST_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 LanguageTestScore createLanguageTestScore(SiteUser siteUser, VerifyStatus status) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(TOEIC, "500", "/toeic-report.pdf"), + siteUser + ); + languageTestScore.setVerifyStatus(status); + return languageTestScoreRepository.save(languageTestScore); + } +} 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 index ecadd8aff..5af4e8399 100644 --- a/src/test/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidatorTest.java +++ b/src/test/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidatorTest.java @@ -1,6 +1,8 @@ package com.example.solidconnection.custom.validation.validator; import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest; +import com.example.solidconnection.admin.dto.LanguageTestScoreUpdateRequest; +import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.type.VerifyStatus; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; @@ -8,6 +10,7 @@ import jakarta.validation.ValidatorFactory; 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 java.util.Set; @@ -28,39 +31,83 @@ void setUp() { 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(); + @Nested + class GPA_점수_거절_사유_검증 { + + @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()); + } } - @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()); + @Nested + class 어학_점수_거절_사유_검증 { + + @Test + void 거절_상태일_때_거절사유가_있으면_유효하다() { + // given + LanguageTestScoreUpdateRequest request = new LanguageTestScoreUpdateRequest( + LanguageTestType.TOEIC, + "900", + VerifyStatus.REJECTED, + "부적합" + ); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 거절_상태일_때_거절사유가_없으면_예외_응답을_반환한다() { + // given + LanguageTestScoreUpdateRequest request = new LanguageTestScoreUpdateRequest( + LanguageTestType.TOEIC, + "900", + VerifyStatus.REJECTED, + null + ); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .extracting(MESSAGE) + .contains(REJECTED_REASON_REQUIRED.getMessage()); + } } }