[Feat] 매칭 점수 계산 및 저장 로직 구현#173
Conversation
|
Warning Review limit reached
More reviews will be available in 37 minutes and 7 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 Walkthrough워크스루사용자 카드 변경 시 매칭 점수를 자동으로 재계산하는 변경 사항매칭 점수 계산 및 동기화
예상 코드 리뷰 소요 시간🎯 3 (중간) | ⏱️ ~20분 관련 PR
시
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java (1)
103-114:⚠️ Potential issue | 🟠 Major | ⚡ Quick win성향 항목 중복 검증이 없어 점수 상한(성향 5점)이 깨질 수 있습니다.
Line 103-114, 176-187에서 동일
traitItemId를 중복 저장할 수 있어calculateTraitScore가 항목 수만큼 가산됩니다. 결과적으로 총점(최대 11) 계약이 깨질 수 있으니,addCourse/updateCourse둘 다 저장 전에 중복 검증을 공통 메서드로 강제해주세요.수정 예시
+ private void validateDuplicateTraitItems(List<Long> traitItemIds) { + if (traitItemIds.size() != traitItemIds.stream().distinct().count()) { + throw new TraitException(TraitErrorCode.DUPLICATE_TRAIT_ITEM); + } + } @@ public void updateCourse(Long userId, Long courseId, UpdateCourseRequestDTO request) { + validateDuplicateTraitItems( + request.getTraits().stream().map(UpdateCourseRequestDTO.TraitDTO::getTraitItemId).toList() + ); UserCourseProfile profile = userCourseProfileRepository @@ public void addCourse(Long userId, AddCourseRequestDTO request) { + validateDuplicateTraitItems( + request.getTraits().stream().map(AddCourseRequestDTO.TraitDTO::getTraitItemId).toList() + ); User user = userRepository.findById(userId)참고: 입력 무결성은 서비스 계층 검증 + DB 유니크 제약(가능하면 profile_id + trait_item_id) 병행이 가장 견고합니다.
Also applies to: 176-187
🤖 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/capstone/pickIt/api/user/service/MypageCourseService.java` around lines 103 - 114, Duplicate traitItemId values are not validated before persisting in MypageCourseService (observed in the loops using request.getTraits() which call traitItemRepository and userCourseTraitRepository.save and lead to calculateTraitScore overcounting); add a shared validation method (e.g., validateNoDuplicateTraits(List<TraitDTO>) called from both addCourse and updateCourse) that checks for duplicate traitItemId in the incoming request and throws a TraitException if found, apply this validation before any UserCourseTrait.builder().build() save loop, and add a DB-level unique constraint on profile_id + trait_item_id to enforce integrity.
🤖 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 `@src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java`:
- Around line 45-52: The current MatchScoreService scanning all users via
userRepository.findAll() and calling calculateScore(user, targetProfile)
triggers N+1 queries (fetching profiles/preferences/team level inside
calculateScore); change to narrow candidates in the DB first (e.g., add
repository query methods or Criteria/JPQL to filter by basic criteria) and load
all required related data in bulk using JOIN FETCH or batch queries (load User
-> Profile, SubjectProfile, Preference, TeamLevel in one query or via repository
methods returning projections/DTOs), then map those pre-fetched entities into
calculateScoreBatch(List<User> candidates, Profile targetProfile) or adapt
calculateScore to accept preloaded data so per-user work is in-memory and avoids
per-entity repository calls in MatchScoreService.
- Around line 127-133: The current logic in MatchScoreService uses
teamLevelRepository.findByUserId(...).map(...).orElse(0) so two users with no
registered level are treated as equal and get max score; change it so if either
findByUserId(myUserId) or findByUserId(targetUserId) is empty the method returns
0 (or the agreed "unknown" policy score) instead of defaulting to 0 for both;
implement by checking the Optionals returned from teamLevelRepository before
computing diff (use isPresent/get or map with orElseThrow) and only compute
Math.max(3 - diff, 0) when both myLevel and targetLevel exist, and add/update a
domain comment in MatchScoreService documenting the unknown-level scoring
policy.
In `@src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java`:
- Line 56: The code in MatchScore that sets
.calculatedAt(LocalDateTime.now(ZoneOffset.UTC)) hardcodes UTC and breaks the
project's Asia/Seoul time contract; replace this with the project-default
LocalDateTime by using LocalDateTime.now() (or, better, use an injected Clock
and call LocalDateTime.now(clock)) so calculatedAt follows the application
timezone; update the MatchScore creation site to accept/use the Clock (or call
LocalDateTime.now()) and remove ZoneOffset.UTC usage to keep consistency across
the codebase.
---
Outside diff comments:
In `@src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java`:
- Around line 103-114: Duplicate traitItemId values are not validated before
persisting in MypageCourseService (observed in the loops using
request.getTraits() which call traitItemRepository and
userCourseTraitRepository.save and lead to calculateTraitScore overcounting);
add a shared validation method (e.g., validateNoDuplicateTraits(List<TraitDTO>)
called from both addCourse and updateCourse) that checks for duplicate
traitItemId in the incoming request and throws a TraitException if found, apply
this validation before any UserCourseTrait.builder().build() save loop, and add
a DB-level unique constraint on profile_id + trait_item_id to enforce integrity.
🪄 Autofix (Beta)
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5c7f335e-f51a-4c0a-8d6c-69e82c528c25
📒 Files selected for processing (5)
src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.javasrc/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.javasrc/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.javasrc/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.javasrc/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java
| List<User> allUsers = userRepository.findAll().stream() | ||
| .filter(u -> !u.getId().equals(targetProfile.getUser().getId())) | ||
| .toList(); | ||
|
|
||
| // 각 유저와의 점수 계산 | ||
| List<MatchScore> scores = allUsers.stream() | ||
| .map(user -> calculateScore(user, targetProfile)) | ||
| .toList(); |
There was a problem hiding this comment.
재계산 경로가 전체 스캔 + N+1 조회 구조라 트래픽 시 급격히 느려질 수 있습니다.
Line 45-52, 66-74에서 전체 로딩 후 스트림 필터링을 하고, 각 항목 계산마다 추가 조회가 발생해(성향/과목 프로필/팀레벨) 재계산 1회당 쿼리 수가 크게 증가합니다. 후보군을 DB에서 먼저 좁히고, 필요한 데이터는 배치 조회로 한 번에 가져오도록 바꾸는 게 안전합니다.
개선 방향 예시
- List<User> allUsers = userRepository.findAll().stream()
- .filter(u -> !u.getId().equals(targetProfile.getUser().getId()))
- .toList();
+ List<Long> candidateUserIds = userCourseProfileRepository
+ .findDistinctUserIdsByCourseIdAndDeletedAtIsNullAndRecruitmentStatusNot(
+ targetProfile.getCourse().getId(),
+ RecruitmentStatus.RECRUITMENT_COMPLETED
+ );
+ // 이후 candidateUserIds 기준으로 default traits / team levels / my profile을 일괄 조회 후 메모리 계산관련 개념: Spring Data JPA에서 projection/배치 조회로 N+1 회피 패턴을 권장합니다.
Also applies to: 66-74, 79-83, 89-90, 112-114, 127-130
🤖 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/capstone/pickIt/api/course/service/MatchScoreService.java`
around lines 45 - 52, The current MatchScoreService scanning all users via
userRepository.findAll() and calling calculateScore(user, targetProfile)
triggers N+1 queries (fetching profiles/preferences/team level inside
calculateScore); change to narrow candidates in the DB first (e.g., add
repository query methods or Criteria/JPQL to filter by basic criteria) and load
all required related data in bulk using JOIN FETCH or batch queries (load User
-> Profile, SubjectProfile, Preference, TeamLevel in one query or via repository
methods returning projections/DTOs), then map those pre-fetched entities into
calculateScoreBatch(List<User> candidates, Profile targetProfile) or adapt
calculateScore to accept preloaded data so per-user work is in-memory and avoids
per-entity repository calls in MatchScoreService.
🔗 관련 이슈
📌 작업 내용
🧪 테스트 결과
📸 스크린샷 (선택)
📎 참고 사항 (선택)
Summary by CodeRabbit
릴리스 노트