diff --git a/.claude/skills/load-universities/scripts/ingest_universities.py b/.claude/skills/load-universities/scripts/ingest_universities.py index 760124a86..21cd95e2a 100644 --- a/.claude/skills/load-universities/scripts/ingest_universities.py +++ b/.claude/skills/load-universities/scripts/ingest_universities.py @@ -376,17 +376,20 @@ def fetch_all_home_universities(api: ApiClient) -> dict[str, dict[str, Any]]: def fetch_all_host_universities(api: ApiClient) -> dict[str, dict[str, Any]]: by_name: dict[str, dict[str, Any]] = {} - page = 0 + page = 1 while True: - response = api.request_json("GET", "/admin/host-universities", query={"page": page, "size": 100}) + response = api.request_json("GET", "/admin/host-universities", query={"page": page, "size": 50}) for item in response.get("content", []): for name_key in ("koreanName", "englishName", "formatName"): name = item.get(name_key) if name: by_name[name] = item + stripped = name.strip() + if stripped and stripped != name: + by_name[stripped] = item total_pages = int(response.get("totalPages", 0)) page += 1 - if page >= total_pages: + if page > total_pages: break return by_name @@ -617,6 +620,8 @@ def verify_row(api: ApiClient, row: ParsedRow, apply_info_id: int, term_id: int, actual = fetched.get(key) if key == "languageRequirements": actual = sorted(actual or [], key=lambda lr: (lr.get("languageTestType"), lr.get("minScore"))) + if key == "koreanName" and isinstance(actual, str) and actual.strip() == expected_value: + continue if actual != expected_value: mismatches.append({"field": key, "expected": expected_value, "actual": actual}) if mismatches: diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 75180f9b0..1e614bdb2 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -192,6 +192,7 @@ public enum ErrorCode { INVALID_MARKDOWN_FORMAT(HttpStatus.BAD_REQUEST.value(), "올바른 마크다운 표 형식이 아닙니다."), // general + INVALID_PAGE_PARAMETER(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 페이지 번호입니다. page는 1 이상의 정수여야 합니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java b/src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java index ecb8bc75b..ef8bf589b 100644 --- a/src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java +++ b/src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java @@ -1,8 +1,16 @@ package com.example.solidconnection.common.resolver; +import static com.example.solidconnection.common.exception.ErrorCode.INVALID_PAGE_PARAMETER; + +import com.example.solidconnection.common.exception.CustomException; +import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; @Component public class CustomPageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver { @@ -10,10 +18,37 @@ public class CustomPageableHandlerMethodArgumentResolver extends PageableHandler private static final int DEFAULT_PAGE = 0; private static final int MAX_SIZE = 50; private static final int DEFAULT_SIZE = 10; + private static final int MIN_ONE_INDEXED_PAGE = 1; public CustomPageableHandlerMethodArgumentResolver() { setMaxPageSize(MAX_SIZE); setOneIndexedParameters(true); setFallbackPageable(PageRequest.of(DEFAULT_PAGE, DEFAULT_SIZE)); } + + @Override + public Pageable resolveArgument( + MethodParameter methodParameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + validatePageParameter(methodParameter, webRequest); + return super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory); + } + + private void validatePageParameter(MethodParameter methodParameter, NativeWebRequest webRequest) { + String parameterName = getParameterNameToUse(getPageParameterName(), methodParameter); + String pageParameter = webRequest.getParameter(parameterName); + if (pageParameter == null || pageParameter.isBlank()) { + return; + } + try { + if (Integer.parseInt(pageParameter) < MIN_ONE_INDEXED_PAGE) { + throw new CustomException(INVALID_PAGE_PARAMETER); + } + } catch (NumberFormatException e) { + // 숫자로 파싱할 수 없는 값은 기존과 동일하게 상위 리졸버가 기본값으로 대체한다. + } + } } diff --git a/src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.java b/src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.java index 9df860078..47d7ceb52 100644 --- a/src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.java +++ b/src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.java @@ -1,7 +1,10 @@ package com.example.solidconnection.common.resolver; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.support.TestContainerSpringBootTest; import java.lang.reflect.Method; import java.util.stream.Stream; @@ -39,12 +42,17 @@ static Stream provideInvalidParameters() { return Stream.of( Arguments.of("null", null), Arguments.of("빈 문자열", ""), - Arguments.of("0", "0"), - Arguments.of("음수", "-1"), Arguments.of("문자열", "invalid") ); } + static Stream provideOutOfRangePageParameters() { + return Stream.of( + Arguments.of("0", "0"), + Arguments.of("음수", "-1") + ); + } + @BeforeEach void setUp() throws NoSuchMethodException { request = new MockHttpServletRequest(); @@ -110,6 +118,20 @@ void setUp() throws NoSuchMethodException { assertThat(pageable.getPageNumber()).isEqualTo(DEFAULT_PAGE); } + @ParameterizedTest(name = "{0}") + @MethodSource("provideOutOfRangePageParameters") + void 페이지_파라미터가_1_미만이면_예외를_던진다(String testName, String pageParam) { + // given + request.setParameter(PAGE_PARAMETER, pageParam); + + // when & then + assertThatThrownBy(() -> customPageableHandlerMethodArgumentResolver + .resolveArgument(parameter, null, webRequest, null)) + .isInstanceOf(CustomException.class) + .extracting(exception -> ((CustomException) exception).getErrorCode()) + .isEqualTo(ErrorCode.INVALID_PAGE_PARAMETER); + } + @ParameterizedTest(name = "{0}") @MethodSource("provideInvalidParameters") void 사이즈_파라미터가_유효하지_않으면_기본_값을_사용한다(String testName, String sizeParam) {