diff --git a/src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java b/src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java new file mode 100644 index 00000000..22b3bcec --- /dev/null +++ b/src/main/java/com/capstone/pickIt/api/course/service/MatchScoreService.java @@ -0,0 +1,150 @@ +package com.capstone.pickIt.api.course.service; + +import com.capstone.pickIt.domain.course.entity.ImportanceLevel; +import com.capstone.pickIt.domain.course.entity.RecruitmentStatus; +import com.capstone.pickIt.domain.course.entity.UserCourseProfile; +import com.capstone.pickIt.domain.course.entity.UserCourseTrait; +import com.capstone.pickIt.domain.course.repository.UserCourseProfileRepository; +import com.capstone.pickIt.domain.matching.entity.MatchScore; +import com.capstone.pickIt.domain.matching.repository.MatchScoreRepository; +import com.capstone.pickIt.domain.teamlevel.entity.TeamLevel; +import com.capstone.pickIt.domain.teamlevel.repository.TeamLevelRepository; +import com.capstone.pickIt.domain.trait.entity.TraitSide; +import com.capstone.pickIt.domain.user.entity.User; +import com.capstone.pickIt.domain.user.entity.UserDefaultTrait; +import com.capstone.pickIt.domain.user.repository.UserDefaultTraitRepository; +import com.capstone.pickIt.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MatchScoreService { + + private final MatchScoreRepository matchScoreRepository; + private final UserCourseProfileRepository userCourseProfileRepository; + private final UserDefaultTraitRepository userDefaultTraitRepository; + private final TeamLevelRepository teamLevelRepository; + private final UserRepository userRepository; + + // 카드 생성/수정 시 → 해당 카드에 대해 모든 유저의 점수 계산 + @Transactional + public void recalculateForProfile(UserCourseProfile targetProfile) { + // 기존 점수 삭제 + matchScoreRepository.deleteByProfileId(targetProfile.getId()); + + // 해당 과목의 다른 유저 목록 조회 + List otherProfiles = userCourseProfileRepository + .findAllByUserIdAndDeletedAtIsNull(targetProfile.getUser().getId()); + + // 같은 과목의 모든 유저 조회 (본인 제외) + List allUsers = userRepository.findAll().stream() + .filter(u -> !u.getId().equals(targetProfile.getUser().getId())) + .toList(); + + // 각 유저와의 점수 계산 + List scores = allUsers.stream() + .map(user -> calculateScore(user, targetProfile)) + .toList(); + + matchScoreRepository.saveAll(scores); + } + + // 기본 팀플 성향 수정 시 → 해당 유저가 조회하는 모든 점수 재계산 + @Transactional + public void recalculateForUser(Long userId) { + User user = userRepository.findById(userId).orElseThrow(); + + // 기존 점수 삭제 + matchScoreRepository.deleteByUserId(userId); + + // 활성화된 모든 카드 조회 (본인 카드 제외) + List allProfiles = userCourseProfileRepository.findAll().stream() + .filter(p -> !p.getUser().getId().equals(userId)) + .filter(p -> p.getDeletedAt() == null) + .filter(p -> p.getRecruitmentStatus() != RecruitmentStatus.RECRUITMENT_COMPLETED) + .toList(); + + List scores = allProfiles.stream() + .map(profile -> calculateScore(user, profile)) + .toList(); + + matchScoreRepository.saveAll(scores); + } + + private MatchScore calculateScore(User user, UserCourseProfile targetProfile) { + int traitScore = calculateTraitScore(user.getId(), targetProfile); + int importanceScore = calculateImportanceScore(user.getId(), targetProfile); + int levelScore = calculateLevelScore(user.getId(), targetProfile.getUser().getId()); + + return MatchScore.of(user, targetProfile, traitScore, importanceScore, levelScore); + } + + // 성향 일치도 (최대 5점) + private int calculateTraitScore(Long userId, UserCourseProfile targetProfile) { + List myTraits = userDefaultTraitRepository.findByUserId(userId); + List targetTraits = targetProfile.getTraits(); + + Map myTraitMap = myTraits.stream() + .collect(Collectors.toMap( + t -> t.getTraitItem().getTraitItemsId(), + UserDefaultTrait::getSelectedSide + )); + + int score = 0; + for (UserCourseTrait targetTrait : targetTraits) { + Long traitItemId = targetTrait.getTraitItem().getTraitItemsId(); + TraitSide mySide = myTraitMap.get(traitItemId); + if (mySide != null && mySide == targetTrait.getSelectedSide()) { + score++; + } + } + return score; + } + + // 중요도 일치도 (최대 3점) + private int calculateImportanceScore(Long userId, UserCourseProfile targetProfile) { + // 내 카드 중 같은 과목 카드 조회 + UserCourseProfile myProfile = userCourseProfileRepository + .findByUserIdAndCourseIdAndDeletedAtIsNull(userId, targetProfile.getCourse().getId()) + .orElse(null); + + if (myProfile == null) return 0; + + int myLevel = importanceToInt(myProfile.getImportanceLevel()); + int targetLevel = importanceToInt(targetProfile.getImportanceLevel()); + int diff = Math.abs(myLevel - targetLevel); + + return Math.max(3 - diff, 0); + } + + // 팀플 레벨 (최대 3점) + private int calculateLevelScore(Long myUserId, Long targetUserId) { + Optional myTeamLevel = teamLevelRepository.findByUserId(myUserId); + Optional targetTeamLevel = teamLevelRepository.findByUserId(targetUserId); + + // 한쪽이라도 레벨 정보 없으면 0점 + if (myTeamLevel.isEmpty() || targetTeamLevel.isEmpty()) { + return 0; + } + + int myLevel = myTeamLevel.get().getLevel(); + int targetLevel = targetTeamLevel.get().getLevel(); + int diff = Math.abs(myLevel - targetLevel); + return Math.max(3 - diff, 0); + } + + private int importanceToInt(ImportanceLevel level) { + return switch (level) { + case HIGH -> 3; + case MEDIUM -> 2; + case LOW -> 1; + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java b/src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java index f75e0166..966099c4 100644 --- a/src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java +++ b/src/main/java/com/capstone/pickIt/api/user/service/MypageCourseService.java @@ -1,5 +1,6 @@ package com.capstone.pickIt.api.user.service; +import com.capstone.pickIt.api.course.service.MatchScoreService; import com.capstone.pickIt.api.user.dto.request.AddCourseRequestDTO; import com.capstone.pickIt.api.user.dto.request.UpdateCourseRequestDTO; import com.capstone.pickIt.api.user.dto.response.CourseCardResponseDTO; @@ -15,6 +16,7 @@ import com.capstone.pickIt.domain.course.repository.UserCourseProfileRepository; import com.capstone.pickIt.domain.course.repository.UserCourseRepository; import com.capstone.pickIt.domain.course.repository.UserCourseTraitRepository; +import com.capstone.pickIt.domain.matching.repository.MatchScoreRepository; import com.capstone.pickIt.domain.trait.entity.TraitItem; import com.capstone.pickIt.domain.trait.exception.TraitErrorCode; import com.capstone.pickIt.domain.trait.exception.TraitException; @@ -44,6 +46,8 @@ public class MypageCourseService { private final TraitItemRepository traitItemRepository; private final UserRepository userRepository; private final ProjectTeamMemberRepository projectTeamMemberRepository; + private final MatchScoreService matchScoreService; + private final MatchScoreRepository matchScoreRepository; @Transactional(readOnly = true) public List getCourseList(Long userId) { @@ -108,6 +112,7 @@ public void updateCourse(Long userId, Long courseId, UpdateCourseRequestDTO requ .build() ); } + matchScoreService.recalculateForProfile(profile); } @Transactional @@ -126,6 +131,9 @@ public void deleteCourse(Long userId, Long courseId) { userCourseTraitRepository.deleteByUserCourseProfileId(profile.getId()); userCourseRepository.deleteByUserIdAndCourseId(userId, courseId); + + matchScoreRepository.deleteByProfileId(profile.getId()); + profile.softDelete(); } @@ -177,5 +185,7 @@ public void addCourse(Long userId, AddCourseRequestDTO request) { .build() ); } + + matchScoreService.recalculateForProfile(profile); } } \ No newline at end of file diff --git a/src/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.java b/src/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.java index b8301483..aa624328 100644 --- a/src/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.java +++ b/src/main/java/com/capstone/pickIt/api/user/service/UserDefaultTraitService.java @@ -1,5 +1,6 @@ package com.capstone.pickIt.api.user.service; +import com.capstone.pickIt.api.course.service.MatchScoreService; import com.capstone.pickIt.api.user.dto.request.UserDefaultTraitRequestDTO; import com.capstone.pickIt.api.user.dto.response.UserDefaultTraitResponseDTO; import com.capstone.pickIt.domain.trait.entity.TraitItem; @@ -25,6 +26,7 @@ public class UserDefaultTraitService { private final UserDefaultTraitRepository userDefaultTraitRepository; private final UserRepository userRepository; private final TraitItemRepository traitItemRepository; + private final MatchScoreService matchScoreService; // 기본 팀플 성향 조회 @Transactional(readOnly = true) @@ -70,6 +72,7 @@ public void saveDefaultTraits(Long userId, List requ public List updateDefaultTraits(Long userId, List requests) { userDefaultTraitRepository.deleteByUserId(userId); saveDefaultTraits(userId, requests); + matchScoreService.recalculateForUser(userId); return getDefaultTraits(userId); } } \ No newline at end of file diff --git a/src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java b/src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java index f5fb3884..32849551 100644 --- a/src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java +++ b/src/main/java/com/capstone/pickIt/domain/matching/entity/MatchScore.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; +import java.time.ZoneOffset; @Entity @Table(name = "match_scores") @@ -42,4 +43,17 @@ public class MatchScore extends CreatedBaseEntity { @Column(name = "calculated_at", nullable = false) private LocalDateTime calculatedAt; + + public static MatchScore of(User user, UserCourseProfile profile, + int traitScore, int importanceScore, int levelScore) { + return MatchScore.builder() + .user(user) + .userCourseProfile(profile) + .traitScore(traitScore) + .importanceScore(importanceScore) + .levelScore(levelScore) + .totalScore(traitScore + importanceScore + levelScore) + .calculatedAt(LocalDateTime.now()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java b/src/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java index eee1fc8e..fb29b2dd 100644 --- a/src/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java +++ b/src/main/java/com/capstone/pickIt/domain/matching/repository/MatchScoreRepository.java @@ -2,6 +2,24 @@ import com.capstone.pickIt.domain.matching.entity.MatchScore; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MatchScoreRepository extends JpaRepository { + + // 특정 유저가 특정 카드에 대해 계산한 점수 삭제 + @Modifying + @Query("DELETE FROM MatchScore ms WHERE ms.user.id = :userId AND ms.userCourseProfile.id = :profileId") + void deleteByUserIdAndProfileId(@Param("userId") Long userId, @Param("profileId") Long profileId); + + // 특정 카드에 대한 모든 점수 삭제 (카드 삭제 시) + @Modifying + @Query("DELETE FROM MatchScore ms WHERE ms.userCourseProfile.id = :profileId") + void deleteByProfileId(@Param("profileId") Long profileId); + + // 특정 유저가 계산한 모든 점수 삭제 (성향 수정 시) + @Modifying + @Query("DELETE FROM MatchScore ms WHERE ms.user.id = :userId") + void deleteByUserId(@Param("userId") Long userId); } \ No newline at end of file