Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Comment on lines +379 to +381

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 👍 / 👎.

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

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.

total_pages = int(response.get("totalPages", 0))
page += 1
if page >= total_pages:
if page > total_pages:
break
return by_name

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "값을 입력할 수 없습니다."),
Expand Down
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

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.

}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -39,12 +42,17 @@ static Stream<Arguments> provideInvalidParameters() {
return Stream.of(
Arguments.of("null", null),
Arguments.of("빈 문자열", ""),
Arguments.of("0", "0"),
Arguments.of("음수", "-1"),
Arguments.of("문자열", "invalid")
);
}

static Stream<Arguments> provideOutOfRangePageParameters() {
return Stream.of(
Arguments.of("0", "0"),
Arguments.of("음수", "-1")
);
}

@BeforeEach
void setUp() throws NoSuchMethodException {
request = new MockHttpServletRequest();
Expand Down Expand Up @@ -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) {
Expand Down
Loading