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 ca0692936..47bac37e1 100644 --- a/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java +++ b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java @@ -10,17 +10,13 @@ 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; 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -35,15 +31,15 @@ public class AdminScoreController { private final AdminGpaScoreService adminGpaScoreService; private final AdminLanguageTestScoreService adminLanguageTestScoreService; - // todo: 추후 커스텀 페이지 객체 & argumentResolver를 적용 필요 @GetMapping("/gpas") public ResponseEntity> searchGpaScores( @Valid @ModelAttribute ScoreSearchCondition scoreSearchCondition, - @PageableDefault(page = 1) Pageable pageable + Pageable pageable ) { - PagingUtils.validatePage(pageable.getPageNumber(), pageable.getPageSize()); - Pageable internalPageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize()); - Page page = adminGpaScoreService.searchGpaScores(scoreSearchCondition, internalPageable); + Page page = adminGpaScoreService.searchGpaScores( + scoreSearchCondition, + pageable + ); return ResponseEntity.ok(PageResponse.of(page)); } @@ -56,15 +52,15 @@ public ResponseEntity updateGpaScore( return ResponseEntity.ok(response); } - // todo: 추후 커스텀 페이지 객체 & argumentResolver를 적용 필요 @GetMapping("/language-tests") public ResponseEntity> searchLanguageTestScores( @Valid @ModelAttribute ScoreSearchCondition scoreSearchCondition, - @PageableDefault(page = 1) Pageable pageable + Pageable pageable ) { - PagingUtils.validatePage(pageable.getPageNumber(), pageable.getPageSize()); - Pageable internalPageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize()); - Page page = adminLanguageTestScoreService.searchLanguageTestScores(scoreSearchCondition, internalPageable); + Page page = adminLanguageTestScoreService.searchLanguageTestScores( + scoreSearchCondition, + pageable + ); return ResponseEntity.ok(PageResponse.of(page)); } diff --git a/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java index 10e468f56..6d16694cc 100644 --- a/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java +++ b/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java @@ -1,7 +1,7 @@ package com.example.solidconnection.config.web; - import com.example.solidconnection.custom.resolver.AuthorizedUserResolver; +import com.example.solidconnection.custom.resolver.CustomPageableHandlerMethodArgumentResolver; import com.example.solidconnection.custom.resolver.ExpiredTokenResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -16,12 +16,14 @@ public class WebMvcConfig implements WebMvcConfigurer { private final AuthorizedUserResolver authorizedUserResolver; private final ExpiredTokenResolver expiredTokenResolver; + private final CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver; @Override public void addArgumentResolvers(List resolvers) { resolvers.addAll(List.of( authorizedUserResolver, - expiredTokenResolver + expiredTokenResolver, + customPageableHandlerMethodArgumentResolver )); } } 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 4b581f79c..1a4e46b72 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -96,10 +96,6 @@ public enum ErrorCode { 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 파싱을 할 수 없습니다."), JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/custom/resolver/CustomPageableHandlerMethodArgumentResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/CustomPageableHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..418c6867f --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/CustomPageableHandlerMethodArgumentResolver.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.custom.resolver; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.stereotype.Component; + +@Component +public class CustomPageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver { + + private static final int DEFAULT_PAGE = 0; + private static final int MAX_SIZE = 50; + private static final int DEFAULT_SIZE = 10; + + public CustomPageableHandlerMethodArgumentResolver() { + setMaxPageSize(MAX_SIZE); + setOneIndexedParameters(true); + setFallbackPageable(PageRequest.of(DEFAULT_PAGE, DEFAULT_SIZE)); + } +} diff --git a/src/main/java/com/example/solidconnection/util/PagingUtils.java b/src/main/java/com/example/solidconnection/util/PagingUtils.java deleted file mode 100644 index 5b4547410..000000000 --- a/src/main/java/com/example/solidconnection/util/PagingUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -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/custom/resolver/CustomPageableHandlerMethodArgumentResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/CustomPageableHandlerMethodArgumentResolverTest.java new file mode 100644 index 000000000..dc628bc67 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/resolver/CustomPageableHandlerMethodArgumentResolverTest.java @@ -0,0 +1,133 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Pageable; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +import java.lang.reflect.Method; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("커스텀 페이지 요청 argument resolver 테스트") +class CustomPageableHandlerMethodArgumentResolverTest { + + private static final String PAGE_PARAMETER = "page"; + private static final String SIZE_PARAMETER = "size"; + private static final int DEFAULT_PAGE = 0; + private static final int DEFAULT_SIZE = 10; + private static final int MAX_SIZE = 50; + + @Autowired + private CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver; + + private MockHttpServletRequest request; + private NativeWebRequest webRequest; + private MethodParameter parameter; + + @BeforeEach + void setUp() throws NoSuchMethodException { + request = new MockHttpServletRequest(); + webRequest = new ServletWebRequest(request); + Method method = TestController.class.getMethod("pageableMethod", Pageable.class); + parameter = new MethodParameter(method, 0); + } + + @Test + void 유효한_페이지_파라미터가_있으면_해당_값을_사용한다() { + // given + int expectedPage = 2; + request.setParameter(PAGE_PARAMETER, String.valueOf(expectedPage)); + + // when + Pageable pageable = customPageableHandlerMethodArgumentResolver + .resolveArgument(parameter, null, webRequest, null); + + // then + assertThat(pageable.getPageNumber()).isEqualTo(expectedPage - 1); + assertThat(pageable.getPageSize()).isEqualTo(DEFAULT_SIZE); + } + + @Test + void 유효한_사이즈_파라미터가_있으면_해당_값을_사용한다() { + // given + int expectedSize = 20; + request.setParameter(SIZE_PARAMETER, String.valueOf(expectedSize)); + + // when + Pageable pageable = customPageableHandlerMethodArgumentResolver + .resolveArgument(parameter, null, webRequest, null); + + // then + assertThat(pageable.getPageNumber()).isEqualTo(DEFAULT_PAGE); + assertThat(pageable.getPageSize()).isEqualTo(expectedSize); + } + + @Test + void 사이즈_파라미터가_최대값을_초과하면_최대값을_사용한다() { + // given + request.setParameter(SIZE_PARAMETER, String.valueOf(MAX_SIZE + 1)); + + // when + Pageable pageable = customPageableHandlerMethodArgumentResolver + .resolveArgument(parameter, null, webRequest, null); + + // then + assertThat(pageable.getPageSize()).isEqualTo(MAX_SIZE); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideInvalidParameters") + void 페이지_파라미터가_유효하지_않으면_기본_값을_사용한다(String testName, String pageParam) { + // given + request.setParameter(PAGE_PARAMETER, pageParam); + + // when + Pageable pageable = customPageableHandlerMethodArgumentResolver + .resolveArgument(parameter, null, webRequest, null); + + // then + assertThat(pageable.getPageNumber()).isEqualTo(DEFAULT_PAGE); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideInvalidParameters") + void 사이즈_파라미터가_유효하지_않으면_기본_값을_사용한다(String testName, String sizeParam) { + // given + request.setParameter(SIZE_PARAMETER, sizeParam); + + // when + Pageable pageable = customPageableHandlerMethodArgumentResolver + .resolveArgument(parameter, null, webRequest, null); + + // then + assertThat(pageable.getPageSize()).isEqualTo(DEFAULT_SIZE); + } + + static Stream provideInvalidParameters() { + return Stream.of( + Arguments.of("null", null), + Arguments.of("빈 문자열", ""), + Arguments.of("0", "0"), + Arguments.of("음수", "-1"), + Arguments.of("문자열", "invalid") + ); + } + + private static class TestController { + + public void pageableMethod(Pageable pageable) { + } + } +} diff --git a/src/test/java/com/example/solidconnection/util/PagingUtilsTest.java b/src/test/java/com/example/solidconnection/util/PagingUtilsTest.java deleted file mode 100644 index f8a10a473..000000000 --- a/src/test/java/com/example/solidconnection/util/PagingUtilsTest.java +++ /dev/null @@ -1,61 +0,0 @@ -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()); - } -}