Skip to content

[Feat] 매칭 점수 계산 및 저장 로직 구현#173

Merged
romdyfo merged 2 commits into
developfrom
feat/matching-172-score-calculation
Jun 8, 2026
Merged

[Feat] 매칭 점수 계산 및 저장 로직 구현#173
romdyfo merged 2 commits into
developfrom
feat/matching-172-score-calculation

Conversation

@romdyfo

@romdyfo romdyfo commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

🔗 관련 이슈

관련된 이슈 번호를 적어주세요.
#172
closes #172

📌 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요.

  • MatchScoreRepository 메서드 추가
  • MatchScoreService 구현
    • 성향 일치도(trait_score) 계산
    • 중요도 일치도(importance_score) 계산
    • 팀플 레벨(level_score) 계산
    • total_score 합산 후 저장
  • 점수 재계산 트리거 연결
    • 카드 생성 시 (addCourse)
    • 카드 수정 시 (updateCourse)
    • 기본 팀플 성향 수정 시 (updateDefaultTraits)

🧪 테스트 결과

Postman 스크린샷, 테스트 통과 여부 등을 첨부해주세요.

📸 스크린샷 (선택)

필요시 스크린샷을 첨부해주세요.

📎 참고 사항 (선택)

리뷰어에게 전달할 내용이 있다면 작성해주세요.

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 과목 프로필을 추가, 수정 또는 삭제할 때 매칭 점수가 자동으로 재계산됩니다.
    • 사용자 성향 설정 변경 시 매칭 점수가 자동으로 갱신됩니다.
    • 매칭 점수는 성향, 중요도, 팀플 레벨을 종합적으로 반영하여 계산됩니다.

@romdyfo romdyfo self-assigned this Jun 8, 2026
@romdyfo romdyfo added the ✨Feature 기능 개발 label Jun 8, 2026
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@romdyfo, we couldn't start this review because you've reached your PR review rate limit.

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 @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 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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 042953af-0bae-4ac2-b42f-a42cda4d104a

📥 Commits

Reviewing files that changed from the base of the PR and between ae7f633 and 5062371.

📒 Files selected for processing (2)
  • src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java
  • src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java
📝 Walkthrough

워크스루

사용자 카드 변경 시 매칭 점수를 자동으로 재계산하는 MatchScoreService를 새로 도입하고, 성향·중요도·팀플 레벨의 세 요소로부터 합산 점수를 계산해 저장한다. 카드 CRUD와 사용자 성향 수정 플로우에 점수 재계산을 연결한다.

변경 사항

매칭 점수 계산 및 동기화

레이어 / 파일 설명
저장소 및 엔티티 계약 확장
src/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java, src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java
MatchScoreRepository에 deleteByUserIdAndProfileId, deleteByProfileId, deleteByUserId 삭제 메서드 3개를 추가하고, MatchScore의 of(...) 팩토리 메서드에서 totalScore를 세 점수의 합으로 계산하며 calculatedAt을 UTC 기준으로 설정한다.
MatchScoreService 채점 로직
src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java
새로운 @Service로서 recalculateForProfile(targetProfile)recalculateForUser(userId) 두 진입점을 통해 매칭 점수를 재계산한다. 각 메서드는 기존 점수를 삭제한 뒤 성향·중요도·레벨을 각각 계산해 MatchScore.of(...)로 생성하고 배치로 저장한다.
카드 CRUD와 점수 동기화
src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java
addCourse에서 프로필 저장 후 점수 재계산, updateCourse에서 트레잇 갱신 후 재계산, deleteCourse에서 소프트 삭제 전 점수 제거를 차례로 추가하여 카드 변경과 점수를 동기화한다.
성향 변경 시 점수 재계산
src/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.java
updateDefaultTraits에서 사용자 기본 성향 저장 후 matchScoreService.recalculateForUser(userId)를 호출해 해당 사용자의 모든 점수를 갱신한다.

예상 코드 리뷰 소요 시간

🎯 3 (중간) | ⏱️ ~20분

관련 PR

  • capstone-pick-it/Backend#95: MypageCourseService의 CRUD 플로우 강화에서 기존 카드 저장·삭제 로직에 매칭 점수 재계산 호출과 삭제 처리를 직렬로 연결한다.
  • capstone-pick-it/Backend#22: MatchScore 엔티티와 MatchScoreRepository 기초 설계를 기반으로 재계산 로직과 삭제 메서드를 추가로 확장한다.

🎯 채점의 마법, 세 가지 점수
성향·중요도·레벨 합산하면 매칭 완성!
카드 변할 때마다 점수 춤을 춘다 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 매칭 점수 계산 및 저장 로직 구현이라는 핵심 변경사항을 명확하게 요약하고 있으며, 추가된 MatchScoreService와 연관된 로직의 주요 목적을 잘 전달합니다.
Description check ✅ Passed PR 설명이 관련 이슈, 작업 내용(MatchScoreRepository 메서드 추가, MatchScoreService 구현, 점수 재계산 트리거)을 포함하고 있으나, 테스트 결과와 스크린샷은 제공되지 않았습니다.
Linked Issues check ✅ Passed PR이 #172의 모든 주요 요구사항을 충족합니다: MatchScoreRepository 메서드 추가, 성향/중요도/레벨 점수 계산 로직 구현, 카드 생성/수정/기본 성향 수정 시 재계산 트리거 연결이 모두 완료되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #172 이슈의 범위 내에서 이루어졌습니다. MatchScoreService, MatchScoreRepository, 관련 서비스 통합이 모두 요구된 매칭 점수 계산 기능 구현에 직접 관련되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/matching-172-score-calculation

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.

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between c2b6d9d and ae7f633.

📒 Files selected for processing (5)
  • src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java
  • src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java
  • src/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.java
  • src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java
  • src/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java

Comment on lines +45 to +52
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

재계산 경로가 전체 스캔 + 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.

Comment thread src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java Outdated
Comment thread src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java Outdated
@romdyfo romdyfo merged commit ec1da6b into develop Jun 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant