-
Notifications
You must be signed in to change notification settings - Fork 8
fix: 페이지네이션 page 파라미터가 1 미만이면 예외를 발생시키도록 #830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
383
to
+392
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# 호스트 대학 이름의 정규화 후 유일성과 중복 검출을 확인합니다.
rg -n -C 6 \
'host-universities|HostUniversity|koreanName|englishName|formatName|unique|Unique|duplicate' \
--glob '*.java' \
--glob '*.py' \
--glob '*.sql' .Repository: solid-connection/solid-connect-server Length of output: 50394 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- HostUniversity definitions and admin API ---'
rg -n -C 12 \
'class HostUniversity|record AdminHostUniversityResponse|host-universities|HostUniversityRepository|findAll|Pageable|koreanName|englishName|formatName' \
src/main/java/com/example/solidconnection/university \
src/main/java/com/example/solidconnection/admin \
--glob '*.java' | head -n 1200
printf '%s\n' '--- Host-university schema and seed data ---'
rg -n -C 8 \
'host_university|host-universit|korean_name|english_name|format_name' \
src/main/resources .claude/skills/load-universities \
--glob '*.sql' --glob '*.csv' --glob '*.json' --glob '*.py' 2>/dev/null | head -n 1200Repository: solid-connection/solid-connect-server Length of output: 50394 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Candidate files ---'
fd -t f 'HostUniversity|host.*univers|Admin.*Univers|University.*Controller|University.*Service|University.*Repository' src/main/java .claude/skills/load-universities
printf '%s\n' '--- Exact host-university references ---'
rg -l 'HostUniversity|host-universities' src/main/java --glob '*.java'
printf '%s\n' '--- Entity constraints and update paths ---'
rg -n -C 10 'class HostUniversity|`@Column`|unique|update|create|save|findAll' \
src/main/java/com/example/solidconnection/university/domain/HostUniversity.java \
src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java \
src/main/java/com/example/solidconnection/admin \
--glob '*.java' | rg -C 4 'HostUniversity|host-universities|koreanName|englishName|formatName|unique|Unique|update|create|findAll'Repository: solid-connection/solid-connect-server Length of output: 50393 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in \
src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java \
src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java \
src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java \
src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java \
src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java \
src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java \
src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java \
src/main/java/com/example/solidconnection/university/domain/HostUniversity.java
do
printf '\n--- %s ---\n' "$f"
cat -n "$f"
done
printf '\n--- Relevant error codes ---\n'
rg -n -C 3 'HOST_UNIVERSITY|UNIVERSITY_ALREADY|DUPLICAT' \
src/main/java --glob '*.java' | head -n 300Repository: solid-connection/solid-connect-server Length of output: 50393 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Ingestion call graph and name lookup ---'
cat -n .claude/skills/load-universities/scripts/ingest_universities.py | sed -n '330,415p;590,645p'
printf '%s\n' '--- Read-only behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
entity = Path("src/main/java/com/example/solidconnection/university/domain/HostUniversity.java").read_text()
service = Path("src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java").read_text()
query = Path("src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java").read_text()
ingest = Path(".claude/skills/load-universities/scripts/ingest_universities.py").read_text()
fields = ("koreanName", "englishName", "formatName")
print("entity_unique_annotations:")
for field in fields:
match = re.search(
rf'`@Column`\([^)]*\)\s*private\s+String\s+{field}\s*;',
entity,
re.S,
)
declaration = match.group(0) if match else "<missing>"
print(f" {field}: {'unique = true' in declaration} | {declaration.replace(chr(10), ' ')}")
print("service_duplicate_checks:")
for field in fields:
count = len(re.findall(rf'findBy{field[0].upper() + field[1:]}\s*\(', service))
print(f" {field}: repository lookup count={count}")
print("query_order:")
order = re.search(r'\.orderBy\(([^)]*)\)', query)
print(f" {order.group(1).strip() if order else '<missing>'}")
print("index_collision_simulation:")
records = [
{"id": 101, "koreanName": "한국대", "englishName": " A", "formatName": "표시A"},
{"id": 202, "koreanName": "다른대", "englishName": "A", "formatName": "표시B"},
]
by_name = {}
for item in records:
for key in fields:
name = item.get(key)
if name:
by_name[name] = item
stripped = name.strip()
if stripped and stripped != name:
by_name[stripped] = item
print(f" lookup['A'] -> id={by_name['A']['id']}")
print(" collision: distinct ids 101 and 202 normalize to the same lookup key")
print("ingestion_error_symbol:")
print(f" IngestionError referenced: {'IngestionError' in ingest}")
PYRepository: solid-connection/solid-connect-server Length of output: 8598 trim() 키 충돌을 감지하고
🤖 Prompt for AI Agents |
||
| 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: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,54 @@ | ||
| 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 { | ||
|
|
||
| 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) { | ||
| // 숫자로 파싱할 수 없는 값은 기존과 동일하게 상위 리졸버가 기본값으로 대체한다. | ||
| } | ||
|
Comment on lines
+46
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java --items all
rg -n -C 4 'Integer\.parseInt|NumberFormatException|INVALID_PAGE_PARAMETER|provideOutOfRangePageParameters' \
src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java \
src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.javaRepository: solid-connection/solid-connect-server Length of output: 7972 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- resolver ---'
cat -n src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java
printf '%s\n' '--- tests (relevant sections) ---'
sed -n '1,155p' src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.java
printf '%s\n' '--- numeric behavior ---'
python3 - <<'PY'
values = ["-1", "0", "1", "-2147483648", "-2147483649", "2147483647", "2147483648", "invalid"]
for value in values:
try:
n = int(value)
result = "INVALID_PAGE_PARAMETER" if n < 1 else "accepted by validation"
except ValueError:
result = "NumberFormatException path"
print(f"{value}: {result}")
PY
printf '%s\n' '--- resolver configuration and usages ---'
rg -n -C 5 'CustomPageableHandlerMethodArgumentResolver|setOneIndexedParameters|resolveArgument\\(' src/main src/testRepository: solid-connection/solid-connect-server Length of output: 8881 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Java parseInt boundary model ---'
python3 - <<'PY'
def java_parse_int(value):
if not value:
raise ValueError("NumberFormatException")
sign = -1 if value[0] == "-" else 1
digits = value[1:] if value[0] in "+-" else value
if not digits or not digits.isdecimal():
raise ValueError("NumberFormatException")
result = sign * int(digits)
if result < -(2**31) or result > 2**31 - 1:
raise ValueError("NumberFormatException")
return result
for value in ["-1", "0", "-2147483648", "-2147483649", "2147483647", "2147483648"]:
try:
parsed = java_parse_int(value)
print(f"{value}: parsed={parsed}, below_one={parsed < 1}")
except ValueError:
print(f"{value}: NumberFormatException")
PY
printf '%s\n' '--- error mapping ---'
rg -n -C 4 'INVALID_PAGE_PARAMETER|enum ErrorCode|CustomException' \
src/main/java src/test/java \
--glob '*.java' | head -160Repository: solid-connection/solid-connect-server Length of output: 19526 1. 범위를 벗어난 음수
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the university-import workflow is run through the tracked Codex skill,
.codex/skills/load-universities/scripts/ingest_universities.py:379-381still initializespage = 0and sends it to/admin/host-universities. The new resolver rejects that first request, so both preflight and apply abort with HTTP 400 before loading any hosts; this commit updates only the.claudecopy. Update the.codexcopy as well, or deduplicate the helpers before enforcing the new minimum.Useful? React with 👍 / 👎.