Skip to content

Conversation

@Gyuhyeok99
Copy link
Contributor

관련 이슈

작업 내용

모의지원 시 지원한 대학 정보 응답을 추가했습니다.

Image

특이 사항

리뷰 요구사항 (선택)

@Gyuhyeok99 Gyuhyeok99 self-assigned this Oct 30, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 30, 2025

Warning

Rate limit exceeded

@Gyuhyeok99 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 15 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 16f4614 and 6055836.

📒 Files selected for processing (2)
  • src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java (4 hunks)
  • src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java (1 hunks)

점검 내용(Walkthrough)

이번 변경사항은 대학 지원 정보 추적 기능을 강화합니다. 먼저, ApplicationSubmissionResponse 레코드에 totalApplyCountappliedUniversities 필드를 추가하고 팩토리 메서드를 from(Application)에서 of(int totalApplyCount, Application, List<UnivApplyInfo>)로 개선했습니다. 동시에 새로운 UnivApplyInfoResponse 레코드를 도입하여 1순위, 2순위, 3순위 대학 선택 정보를 JSON으로 직렬화합니다. ApplicationSubmissionServiceUnivApplyInfoRepository 의존성을 추가하여 대학 선택 ID들을 조회하고, firstChoiceUnivApplyInfoId의 타입을 원시형 long에서 Long으로 변경했습니다. 테스트는 새로운 응답 필드들의 유효성을 검증하는 어설션을 추가했습니다.

코드 리뷰 난이도 평가

🎯 3 (Moderate) | ⏱️ ~20 분

  • UnivApplyInfoResponse의 매핑 로직: ID를 대학 한글명으로 변환하는 과정에서 맵 구성과 null 처리가 올바르게 이루어지는지 확인 필요
  • ApplicationSubmissionService의 흐름 변경: 저장소 조회 로직, null 필터링, 새로운 팩토리 메서드 호출 체인이 정확한지 검증 필요
  • 타입 변경의 영향도: firstChoiceUnivApplyInfoIdlong에서 Long으로 변경됨에 따른 하위 호출 지점의 호환성 확인
  • 테스트 커버리지: 새로 추가된 필드 검증이 모든 시나리오를 포함하는지 검토

제안하는 리뷰어

  • wibaek
  • whqtker

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed 제목 "refactor: 모의지원 시 지원한 대학 정보 응답 추가"는 변경사항의 핵심을 명확하고 간결하게 설명하고 있습니다. 모의지원 기능에서 대학 정보 응답을 추가하는 것이 주요 목표이며, 이는 ApplicationSubmissionResponse의 구조 변경, UnivApplyInfoResponse 신규 도입, ApplicationSubmissionService의 로직 확장, 그리고 테스트 개선 등 모든 변경사항을 적절히 대표합니다. 제목은 가독성이 뛰어나고 불필요한 표현이나 모호함이 없습니다.
Linked Issues Check ✅ Passed 이슈 #531의 요구사항이 완전히 구현되었습니다. 첫째, ApplicationSubmissionResponse에 totalApplyCount, applyCount, appliedUniversities 필드를 추가하여 기존 지원 횟수에 대학 정보를 함께 응답하도록 개선했습니다. 둘째, UnivApplyInfoResponse 새로운 record를 도입하여 1·2·3지망 대학 정보를 JSON 형태로 매핑했습니다. 셋째, ApplicationSubmissionService에서 UnivApplyInfoRepository를 활용해 대학 정보를 조회 및 응답에 포함시켰습니다. 넷째, ApplicationSubmissionServiceTest에서 totalApplyCount와 appliedUniversities의 대학명이 정확하게 응답되는지 검증하는 단계를 추가했습니다.
Out of Scope Changes Check ✅ Passed 모든 코드 변경사항이 이슈 #531의 요구사항 범위 내에 있습니다. 구체적으로, 다음 네 가지 변경 영역이 모두 모의지원 응답에 대학 정보를 추가하는 목표를 지원합니다: 1) ApplicationSubmissionResponse의 필드 확장, 2) UnivApplyInfoResponse의 신규 도입, 3) ApplicationSubmissionService의 대학 정보 조회 및 응답 생성 로직, 4) 테스트의 검증 강화입니다. 범위를 벗어난 변경사항이나 불필요한 리팩토링은 발견되지 않았습니다.
Description Check ✅ Passed PR 설명은 저장소의 템플릿 요구사항을 충분히 충족합니다. 필수 섹션인 "관련 이슈"에서 #531을 명시하였고, "작업 내용"에서 모의지원 시 대학 정보 응답 추가를 설명하면서 변경사항을 시각화한 이미지를 첨부했습니다. 선택사항인 "특이 사항"과 "리뷰 요구사항" 섹션이 비어있지만, 이는 템플릿상 선택사항이므로 필수 요구사항은 모두 충족되었습니다.

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 and usage tips.

@Gyuhyeok99 Gyuhyeok99 marked this pull request as ready for review October 30, 2025 12:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4ff858 and 16f4614.

📒 Files selected for processing (4)
  • src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java (1 hunks)
  • src/main/java/com/example/solidconnection/application/dto/UnivApplyInfoResponse.java (1 hunks)
  • src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java (4 hunks)
  • src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment on lines +87 to 98
List<Long> univApplyInfoIds = Stream.of(
firstChoiceUnivApplyInfoId,
secondChoiceUnivApplyInfoId,
thirdChoiceUnivApplyInfoId
)
.filter(Objects::nonNull)
.toList();

List<UnivApplyInfo> uniApplyInfos = univApplyInfoRepository.findAllByIds(univApplyInfoIds);

return ApplicationSubmissionResponse.of(APPLICATION_UPDATE_COUNT_LIMIT, newApplication, uniApplyInfos);
}
Copy link

@coderabbitai coderabbitai bot Oct 30, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

필수 대학 정보 누락 시 즉시 검증 필요
findAllByIds가 전달한 ID를 모두 돌려주지 못하면 firstChoiceUnivApplyInfoId가 map에 존재하지 않아 응답의 firstChoiceUniversity가 null로 직행합니다. 이번 변경으로 해당 필드는 클라이언트에 반드시 내려가야 하는 값인데, 관리자 삭제·동시성 등 현실적인 상황에서 바로 계약이 깨지고 프런트는 NPE나 렌더링 오류를 겪게 됩니다. 저장 전·후 중 한 곳에서라도 조회 결과에 firstChoiceUnivApplyInfoId가 포함돼 있는지 확인하고, 누락 시 CustomException으로 즉시 중단해 주세요.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ coderabbitai
firstChoiceUnivApplyInfoId 이값은 무조건 존재한다는 가정인데 검증해야해?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check ApplyRequest and UnivApplyInfoChoiceRequest for validation logic
echo "=== ApplyRequest validation ==="
fd -e java -x cat {} \; | rg -A 10 "class ApplyRequest|record ApplyRequest" 

echo -e "\n=== UnivApplyInfoChoiceRequest validation ==="
fd -e java -x cat {} \; | rg -A 10 "class UnivApplyInfoChoiceRequest|record UnivApplyInfoChoiceRequest"

# Check Application entity for relationships/constraints
echo -e "\n=== Application entity relationships ==="
fd -e java -x cat {} \; | rg -B 5 -A 3 "firstChoiceUnivApplyInfoId|@ManyToOne|@JoinColumn" | head -50

# Check UnivApplyInfo entity
echo -e "\n=== UnivApplyInfo entity definition ==="
ast-grep --pattern 'class UnivApplyInfo {
  $$$
}'

# Check UnivApplyInfoRepository.findAllByIds implementation
echo -e "\n=== UnivApplyInfoRepository findAllByIds ==="
fd -e java -x cat {} \; | rg -B 3 -A 5 "findAllByIds"

Length of output: 3379


🏁 Script executed:

#!/bin/bash
# Find where isFirstChoiceNotSelected is used - likely a validator
echo "=== Finding validator usage ==="
rg -n -B 5 -A 15 "isFirstChoiceNotSelected|UnivApplyInfoValidator|class.*Validator" --type java

# Check controller for validation
echo -e "\n=== Application controller apply method ==="
rg -n -B 3 -A 20 "@PostMapping.*apply|public.*apply.*ApplyRequest" --type java | head -80

Length of output: 36426


@Gyuhyeok99

네, 요청 단계에서는 ValidUnivApplyInfoChoiceValidator가 firstChoiceUnivApplyInfoId의 null 여부를 검증하고 있습니다.

하지만 제가 우려한 부분은 조금 다른 시나리오입니다:

현재 상황:

  1. 요청 검증 통과 (firstChoiceUnivApplyInfoId = 123L, null 아님) ✅
  2. Application 저장 ✅
  3. findAllByIds로 UnivApplyInfo 조회
  4. 만약 ID=123 레코드가 DB에 없다면? → map에서 get(123L) = null
  5. firstChoiceUniversity = null → API 응답에서 필수 필드가 null

기술적 격차:

  • Application 엔티티가 UnivApplyInfo ID를 Long 필드로만 저장 (외래키 제약 없음)
  • DB 레벨 참조 무결성이 없어서 UnivApplyInfo 레코드 삭제 가능
  • firstChoiceUniversity는 @JsonInclude(NON_NULL) 없이 필수 응답 필드

선택지:

  1. 런타임 검증 추가 - 조회 후 firstChoice ID가 결과에 포함됐는지 확인하고 누락 시 예외 발생
  2. FK 제약 추가 - Application 엔티티에 @ManyToOne 관계 추가해 DB 레벨 보장
  3. 현상 유지 - 비즈니스 레이어에서 삭제하지 않는다는 가정으로 진행

어떤 방향이 좋을까요? 프로덕션 환경에서 관리자 실수나 데이터 정합성 이슈를 고려하면 1번이나 2번이 안전하지만, 오버헤드를 고려하면 3번도 선택지입니다.


🧬 Code Graph Analysis Results

[UnivApplyInfoQueryService.java:16-55]

@RequiredArgsConstructor
@Service
public class UnivApplyInfoQueryService {

    private final UnivApplyInfoRepository univApplyInfoRepository;

    /*
     * 대학교 상세 정보를 불러온다.
     * - 대학교(University) 정보와 대학 지원 정보(UniversityInfoForApply) 정보를 조합하여 반환한다.
     * */
    @Transactional(readOnly = true)
    @ThunderingHerdCaching(key = "univApplyInfo:{0}:{1}", cacheManager = "customCacheManager", ttlSec = 86400)
    public UnivApplyInfoDetailResponse getUnivApplyInfoDetail(Long univApplyInfoId) {
        UnivApplyInfo univApplyInfo
                = univApplyInfoRepository.getUnivApplyInfoById(univApplyInfoId);
        University university = unvApplyInfo.getUniversity();

        return UnivApplyInfoDetailResponse.of(university, univApplyInfo);
    }

    @Transactional(readOnly = true)
    public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFilterSearchRequest request, String term) {
        List<UnivApplyInfoPreviewResponse> responses = univApplyInfoRepository
                .findAllByFilter(request.languageTestType(), request.testScore(), term, request.countryCode())
                .stream()
                .map(UnivApplyInfoPreviewResponse::from)
                .toList();
        return new UnivApplyInfoPreviewResponses(responses);
    }

    @Transactional(readOnly = true)
    @ThunderingHerdCaching(key = "univApplyInfoTextSearch:{0}:{1}", cacheManager = "customCacheManager", ttlSec = 86400)
    public UnivApplyInfoPreviewResponses searchUnivApplyInfoByText(String text, String term) {
        List<UnivApplyInfoPreviewResponse> responses = univApplyInfoRepository.findAllByText(text, term)
                .stream()
                .map(UnivApplyInfoPreviewResponse::from)
                .toList();
        return new UnivApplyInfoPreviewResponses(responses);
    }
}

[ApplicationQueryService.java:30-136]

@RequiredArgsConstructor
@Service
public class ApplicationQueryService {

    private final ApplicationRepository applicationRepository;
    private final UnivApplyInfoRepository univApplyInfoRepository;
    private final UnivApplyInfoFilterRepositoryImpl universityFilterRepository;
    private final SiteUserRepository siteUserRepository;

    @Value("${university.term}")
    public String term;

    // todo: 캐싱 정책 변경 시 수정 필요
    @Transactional(readOnly = true)
    public ApplicationsResponse getApplicants(long siteUserId, String regionCode, String keyword) {
        // 1. 대학 지원 정보 필터링 (regionCode, keyword)
        SiteUser siteUser = siteUserRepository.findById(siteUserId)
                .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
        List<UnivApplyInfo> univApplyInfos = universityFilterRepository.findAllByRegionCodeAndKeywords(regionCode, List.of(keyword));
        if (univApplyInfos.isEmpty()) {
            return new ApplicationsResponse(List.of(), List.of(), List.of());
        }
        // 2. 조건에 맞는 모든 Application 한 번에 조회
        List<Long> univApplyInfoIds = univApplyInfos.stream()
                .map(UnivApplyInfo::getId)
                .toList();
        List<Application> applications = applicationRepository.findAllByUnivApplyInfoIds(univApplyInfoIds, VerifyStatus.APPROVED, term);
        // 3. 지원서 분류 및 DTO 변환
        return classifyApplicationsByChoice(univApplyInfos, applications, siteUser);
    }

    @Transactional(readOnly = true)
    public ApplicationsResponse getApplicantsByUserApplications(long siteUserId) {
        SiteUser siteUser = siteUserRepository.findById(siteUserId)
                .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
        Application userLatestApplication = applicationRepository.getApplicationBySiteUserIdAndTerm(siteUser.getId(), term);

        List<Long> univApplyInfoIds = Stream.of(
                        userLatestApplication.getFirstChoiceUnivApplyInfoId(),
                        userLatestApplication.getSecondChoiceUnivApplyInfoId(),
                        userLatestApplication.getThirdChoiceUnivApplyInfoId()
                )
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        if (univApplyInfoIds.isEmpty()) {
            return new ApplicationsResponse(List.of(), List.of(), List.of());
        }

        List<Application> applications = applicationRepository.findAllByUnivApplyInfoIds(univApplyInfoIds, VerifyStatus.APPROVED, term);
        List<UnivApplyInfo> univApplyInfos = univApplyInfoRepository.findAllByIds(univApplyInfoIds);

        return classifyApplicationsByChoice(univApplyInfos, applications, siteUser);
    }

    private ApplicationsResponse classifyApplicationsByChoice(
            List<UnivApplyInfo> univApplyInfos,
            List<Application> applications,
            SiteUser siteUser) {
        Map<Long, List<Application>> firstChoiceMap = createChoiceMap(applications, Application::getFirstChoiceUnivApplyInfoId);
        Map<Long, List<Application>> secondChoiceMap = createChoiceMap(applications, Application::getSecondChoiceUnivApplyInfoId);
        Map<Long, List<Application>> thirdChoiceMap = createChoiceMap(applications, Application::getThirdChoiceUnivApplyInfoId);

        List<ApplicantsResponse> firstChoiceApplicants =
                createUniversityApplicantsResponses(univApplyInfos, firstChoiceMap, siteUser);
        List<ApplicantsResponse> secondChoiceApplicants =
                createUniversityApplicantsResponses(univApplyInfos, secondChoiceMap, siteUser);
        List<ApplicantsResponse> thirdChoiceApplicants =
                createUniversityApplicantsResponses(univApplyInfos, thirdChoiceMap, siteUser);

        return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants);
    }

    private Map<Long, List<Application>> createChoiceMap(
            List<Application> applications,
            Function<Application, Long> choiceIdExtractor) {
        Map<Long, List<Application>> choiceMap = new HashMap<>();

        for (Application application : applications) {
            Long choiceId = choiceIdExtractor.apply(application);
            if (choiceId != null) {
                choiceMap.computeIfAbsent(choiceId, k -> new ArrayList<>()).add(application);
            }
        }

        return choiceMap;
    }

    private List<ApplicantsResponse> createUniversityApplicantsResponses(
            List<UnivApplyInfo> univApplyInfos,
            Map<Long, List<Application>> choiceMap,
            SiteUser siteUser) {
        return univApplyInfos.stream()
                .map(uia -> ApplicantsResponse.of(uia, choiceMap.getOrDefault(uia.getId(), List.of()), siteUser))
                .toList();
    }

    @Transactional(readOnly = true)
    public void validateSiteUserCanViewApplicants(long siteUserId) {
        SiteUser siteUser = siteUserRepository.findById(siteUserId)
                .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
        VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserIdAndTerm(siteUser.getId(), term).getVerifyStatus();
        if (verifyStatus != VerifyStatus.APPROVED) {
            throw new CustomException(APPLICATION_NOT_APPROVED);
        }
    }
}

🧠 Learnings used
Learnt from: nayonsoso
PR: solid-connection/solid-connect-server#486
File: src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java:166-178
Timestamp: 2025-08-26T05:00:56.556Z
Learning: UnivApplyInfo의 텍스트 검색 기능에서는 University.koreanName이 아닌 UnivApplyInfo.koreanName을 검색 대상으로 사용하는 것이 의도된 설계이다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

다른 분들 의견 듣고 판단하겠습니다!

Copy link

Choose a reason for hiding this comment

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

@Gyuhyeok99 네, 좋습니다! 팀 논의 후 결정하시면 됩니다. 추가로 도움이 필요하시면 언제든 말씀해 주세요. 😊


🧠 Learnings used
Learnt from: nayonsoso
PR: solid-connection/solid-connect-server#486
File: src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java:166-178
Timestamp: 2025-08-26T05:00:56.556Z
Learning: UnivApplyInfo의 텍스트 검색 기능에서는 University.koreanName이 아닌 UnivApplyInfo.koreanName을 검색 대상으로 사용하는 것이 의도된 설계이다.

Copy link
Contributor

Choose a reason for hiding this comment

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

다른 분들 의견 듣고 판단하겠습니다!

저도 해당 id는 무조건 존재하는 상황에서 요청이 온다 생각하기 때문에 추가적인 검증이 필요하지 않다고 생각이 듭니다.
그러나 혹시라도 매우 낮은 확률로 서비스 운영 중에 db 데이터가 잘못되었거나(예: 다른 작업중 db 오류 및 요청 중 해당 학교가 교환 학생 지원이 유효하지 않은 상태가 되어 db에 반영했을 때 -- 사실 그럴일이 없을 것 같긴 합니다.) 지원 자체가 안되는 상황을 유도하기 위해 검증하는 것도 좋다고 생각이 듭니다!

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

@Hexeong Hexeong Nov 3, 2025

Choose a reason for hiding this comment

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

대학 ID의 경우, 사용자가 직접 값을 입력하는 변수는 아니다 보니 저도 해당 검증은 오버헤드라는 생각이 드네요! 방어적 프로그래밍 관점에서도 대학 ID는 사용자에 의해 값이 관리되지 않기 때문에 저는 굳이라는 생각이 듭니다~

# Conflicts:
#	src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java
Copy link
Contributor

@Hexeong Hexeong left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~!

@Gyuhyeok99 Gyuhyeok99 merged commit 093b6af into solid-connection:develop Nov 3, 2025
2 checks passed
Gyuhyeok99 added a commit that referenced this pull request Nov 4, 2025
fix: PostRepository & CommentRepository의 일부 메서드의 정렬 순서 오류 해결 (#522) 
refactor: 이메일 유니크키 제거
refactor: 엔티티가 BaseEntity를 상속하도록 (#524) 
fix: BaseEntity 관련 마이그레이션 파일 수정 (#530) 
feat: 멘토 승격 api 구현 (#532) 
fix: 대학교 중복 오류 수정 (#510) 
refactor: 불필요한 로그 삭제 (#543)
fix: 운영환경 8081 포트 설정 추가 (#542) 
fix: dev환경 디비명 변경 (#546)
refactor: 불필요한 로그 삭제 (#547) 
refactor: 학기를 테이블로 관리하도록 변경 (#526) 
refactor: 모의지원 시 지원한 대학 정보 응답 추가 (#539)
@Gyuhyeok99 Gyuhyeok99 mentioned this pull request Nov 4, 2025
@Gyuhyeok99 Gyuhyeok99 deleted the refactor/531-add-university-info branch November 9, 2025 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 모의지원 시 지원한 대학 정보 응답 추가

3 participants