Skip to content

fix: 페이지네이션 page 파라미터가 1 미만이면 예외를 발생시키도록 - #830

Open
whqtker wants to merge 3 commits into
developfrom
fix/829-admin-host-universities-pagination
Open

fix: 페이지네이션 page 파라미터가 1 미만이면 예외를 발생시키도록#830
whqtker wants to merge 3 commits into
developfrom
fix/829-admin-host-universities-pagination

Conversation

@whqtker

@whqtker whqtker commented Aug 11, 2026

Copy link
Copy Markdown
Member

관련 이슈

작업 내용

특이 사항

리뷰 요구사항 (선택)

whqtker and others added 2 commits August 11, 2026 14:28
GET /admin/host-universities 등에서 page=0/1이 모두 내부 오프셋 0으로
클램프되어 첫 페이지가 중복 조회되고 마지막 페이지가 영구 누락되는
문제(#829)의 근본 원인은 fetch join이 아니라, CustomPageableHandlerMethod
ArgumentResolver의 1-indexed 계약을 0-indexed로 오사용한 클라이언트였다.

page<1 요청을 조용히 기본값으로 클램프하는 대신 400으로 명시적으로
거부해 향후 동일한 오사용이 재발해도 즉시 드러나도록 한다.

ingest_universities.py의 임시 keyword 우회 방식도 1-indexed 벌크 fetch로
복원.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTCqRi8V2TJ1ZqHpfiQ1vy
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTCqRi8V2TJ1ZqHpfiQ1vy
@whqtker whqtker self-assigned this Aug 11, 2026
@whqtker whqtker added the 버그 Something isn't working label Aug 11, 2026
@whqtker
whqtker requested a review from sukangpunch as a code owner August 11, 2026 05:41
@whqtker whqtker added the 최종 리뷰 최소 1명 필수 label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

  1. 대학 데이터 수집: 조회를 1페이지와 50개 단위로 변경하고 totalPages까지 처리합니다. 이름의 양끝 공백 제거값을 인덱스에 추가합니다.
  2. 페이지 파라미터 검증: page가 1 미만이면 INVALID_PAGE_PARAMETER 예외를 발생시킵니다. 관련 파라미터 테스트를 추가했습니다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: wibaek, hexeong

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning 이슈 번호와 섹션은 포함했지만 작업 내용과 특이 사항이 작성되지 않아 주요 변경사항을 확인하기 어렵습니다. 작업 내용에 page 파라미터 검증과 1-based 페이지네이션 변경을 작성하고, 특이 사항에 API 동작 변경과 영향 범위를 추가하세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed [#829]의 1-based 페이지 처리와 page<1 요청의 오류 반환 요구를 코드와 테스트로 반영했습니다.
Out of Scope Changes check ✅ Passed 변경 사항은 페이지네이션 오류 처리, 대학 조회 수정, 관련 오류 코드와 테스트로 제한되어 범위를 벗어나지 않습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed 제목이 page 파라미터의 1 미만 값을 거부하는 핵심 변경을 정확하고 간결하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/829-admin-host-universities-pagination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTCqRi8V2TJ1ZqHpfiQ1vy

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/load-universities/scripts/ingest_universities.py:
- Around line 383-392: Update the by_name construction in the university
ingestion flow to detect when different records produce the same trimmed name
key and raise IngestionError before overwriting the existing mapping. Apply this
collision check to the stripped aliases for koreanName, englishName, and
formatName while preserving valid exact-name mappings and allowing the same
record to reuse a key.

In
`@src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java`:
- Around line 46-52: Update the page validation in
CustomPageableHandlerMethodArgumentResolver to parse and compare out-of-range
negative values without allowing NumberFormatException to bypass
INVALID_PAGE_PARAMETER; use BigInteger or equivalent arbitrary-precision
validation against MIN_ONE_INDEXED_PAGE, and add boundary tests covering values
below Integer.MIN_VALUE.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a79d696-6675-47bf-b1ef-a9724bf45b8a

📥 Commits

Reviewing files that changed from the base of the PR and between d62ebe0 and 4d46061.

📒 Files selected for processing (4)
  • .claude/skills/load-universities/scripts/ingest_universities.py
  • src/main/java/com/example/solidconnection/common/exception/ErrorCode.java
  • src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java
  • src/test/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolverTest.java

Comment on lines 383 to +392
for name_key in ("koreanName", "englishName", "formatName"):
name = item.get(name_key)
if name:
by_name[name] = item
# parse_rows() always strips field values via clean(), so a DB
# record whose name has stray whitespace (a data-entry artifact)
# would otherwise never match row.host_korean_name/english_name.
stripped = name.strip()
if stripped and stripped != name:
by_name[stripped] = item

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 1200

Repository: 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 300

Repository: 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}")
PY

Repository: solid-connection/solid-connect-server

Length of output: 8598


trim() 키 충돌을 감지하고 IngestionError로 중단해 주세요.

HostUniversitykoreanName만 정확한 값에 대해 유일성을 보장합니다. englishNameformatName에는 유일성 제약과 중복 검증이 없습니다. 따라서 서로 다른 레코드가 trim 후 같은 키를 만들면 후속 레코드가 by_name 값을 덮어쓰고 잘못된 host["id"]를 선택합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/load-universities/scripts/ingest_universities.py around lines
383 - 392, Update the by_name construction in the university ingestion flow to
detect when different records produce the same trimmed name key and raise
IngestionError before overwriting the existing mapping. Apply this collision
check to the stripped aliases for koreanName, englishName, and formatName while
preserving valid exact-name mappings and allowing the same record to reuse a
key.

Comment on lines +46 to +52
try {
if (Integer.parseInt(pageParameter) < MIN_ONE_INDEXED_PAGE) {
throw new CustomException(INVALID_PAGE_PARAMETER);
}
} catch (NumberFormatException e) {
// 숫자로 파싱할 수 없는 값은 기존과 동일하게 상위 리졸버가 기본값으로 대체한다.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.java

Repository: 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/test

Repository: 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 -160

Repository: solid-connection/solid-connect-server

Length of output: 19526


1. 범위를 벗어난 음수 page를 거부하세요.

Integer.parseInt-2147483649NumberFormatException으로 처리합니다. 현재 코드는 이 예외를 무시하므로 INVALID_PAGE_PARAMETER가 발생하지 않습니다. BigInteger 등으로 값을 비교하고 경계값 테스트를 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/solidconnection/common/resolver/CustomPageableHandlerMethodArgumentResolver.java`
around lines 46 - 52, Update the page validation in
CustomPageableHandlerMethodArgumentResolver to parse and compare out-of-range
negative values without allowing NumberFormatException to bypass
INVALID_PAGE_PARAMETER; use BigInteger or equivalent arbitrary-precision
validation against MIN_ONE_INDEXED_PAGE, and add boundary tests covering values
below Integer.MIN_VALUE.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d460611cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +379 to +381
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})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the Codex ingestion helper for one-indexed pages

When the university-import workflow is run through the tracked Codex skill, .codex/skills/load-universities/scripts/ingest_universities.py:379-381 still initializes page = 0 and 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 .claude copy. Update the .codex copy as well, or deduplicate the helpers before enforcing the new minimum.

Useful? React with 👍 / 👎.

@whqtker whqtker changed the title fix: 페이지네이션 page 파라미터가 1 미만이면 400으로 거부하도록 수정 fix: 페이지네이션 page 파라미터가 1 미만이면 예외를 발생시키도록 Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

버그 Something isn't working 최종 리뷰 최소 1명 필수

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: 파견 대학 조회 페이지네이션에서 레코드 누락 및 중복 문제 수정

1 participant