Skip to content
Merged
22 changes: 20 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'org.festiamte'
group = 'org.festimate'
version = '0.0.1-SNAPSHOT'

java {
Expand Down Expand Up @@ -58,8 +58,26 @@ dependencies {
testImplementation 'io.projectreactor:reactor-test' // WebFlux 테스트 지원
testImplementation 'org.springframework.security:spring-security-test' // Security 테스트 지원
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // JUnit 테스트 실행기

// --- QueryDSL ---
implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'

// --- Jakarta Persistence API ---
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
}

sourceSets {
main {
java {
srcDir 'src/main/java'
// Q-타입 생성 위치를 IDE가 인식하도록 추가
srcDir 'build/generated/sources/annotationProcessor/java/main'
}
}
}

tasks.named('test') {
useJUnitPlatform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,7 @@
import java.util.List;
import java.util.Optional;

public interface MatchingRepository extends JpaRepository<Matching, Long> {
@Query("""
SELECT p FROM Participant p
JOIN FETCH p.user
LEFT JOIN Matching m1
ON (m1.applicantParticipant = p OR m1.targetParticipant = p)
WHERE p.festival.festivalId = :festivalId
AND p.typeResult = :typeResult
AND p.user.gender != :gender
AND p.participantId != :participantId
AND p.participantId NOT IN (
SELECT m.targetParticipant.participantId
FROM Matching m
WHERE m.applicantParticipant.participantId = :participantId
AND m.status = 'COMPLETED'
)
GROUP BY p
ORDER BY COUNT(m1) ASC
""")
List<Participant> findMatchingCandidates(
@Param("participantId") Long participantId,
@Param("typeResult") TypeResult typeResult,
@Param("gender") Gender gender,
@Param("festivalId") Long festivalId,
Pageable pageable
);


public interface MatchingRepository extends JpaRepository<Matching, Long>, MatchingRepositoryCustom {
@Query("""
SELECT m FROM Matching m
WHERE m.festival.festivalId = :festivalId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.festimate.team.domain.matching.repository;

import org.festimate.team.domain.participant.entity.Participant;
import org.festimate.team.domain.participant.entity.TypeResult;
import org.festimate.team.domain.user.entity.Gender;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface MatchingRepositoryCustom {
List<Long> findExcludeIds(Long applicantId);

List<Participant> findMatchingCandidatesDsl(
Long applicantId,
TypeResult typeResult,
Gender gender,
Long festivalId,
Pageable pageable,
List<Long> excludedIds
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.festimate.team.domain.matching.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.festimate.team.domain.matching.entity.MatchingStatus;
import org.festimate.team.domain.matching.entity.QMatching;
import org.festimate.team.domain.participant.entity.Participant;
import org.festimate.team.domain.participant.entity.QParticipant;
import org.festimate.team.domain.participant.entity.TypeResult;
import org.festimate.team.domain.user.entity.Gender;
import org.festimate.team.domain.user.entity.QUser;
import org.springframework.data.domain.Pageable;

import java.util.List;

@RequiredArgsConstructor
public class MatchingRepositoryImpl implements MatchingRepositoryCustom {

private final JPAQueryFactory queryFactory;

private final QParticipant p = QParticipant.participant;
private final QMatching m = QMatching.matching;
private final QMatching mSub = new QMatching("mSub");
private final QUser u = QUser.user;

@Override
public List<Long> findExcludeIds(Long applicantId) {
return queryFactory
.select(mSub.targetParticipant.participantId)
.from(mSub)
.where(
mSub.applicantParticipant.participantId.eq(applicantId)
.and(mSub.status.eq(MatchingStatus.COMPLETED))
)
.fetch();
}

@Override
public List<Participant> findMatchingCandidatesDsl(
Long applicantId,
TypeResult typeResult,
Gender gender,
Long festivalId,
Pageable pageable,
List<Long> excludedIds
) {
return queryFactory
.selectFrom(p)
.join(p.user, u).fetchJoin()
.leftJoin(m)
.on(m.applicantParticipant.eq(p).or(m.targetParticipant.eq(p)))
.where(
p.festival.festivalId.eq(festivalId),
p.typeResult.eq(typeResult),
u.gender.ne(gender),
p.participantId.ne(applicantId),
excludedIds.isEmpty() ? null : p.participantId.notIn(excludedIds)
)
.groupBy(p)
.orderBy(m.count().asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,18 @@ protected Matching saveMatching(Festival festival, Optional<Participant> targetP
@Transactional
@Override
public Optional<Participant> findBestCandidateByPriority(long festivalId, Participant participant) {
List<Long> excludedIds = matchingRepository.findExcludeIds(participant.getParticipantId());
List<TypeResult> priorities = MATCHING_PRIORITIES.get(participant.getTypeResult());
Gender myGender = participant.getUser().getGender();

for (TypeResult priorityType : priorities) {
Optional<Participant> candidate = matchingRepository.findMatchingCandidates(
Optional<Participant> candidate = matchingRepository.findMatchingCandidatesDsl(
participant.getParticipantId(),
priorityType,
myGender,
festivalId,
PageRequest.of(0, 1)
PageRequest.of(0, 1),
excludedIds
).stream().findFirst();

if (candidate.isPresent()) {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/festimate/team/infra/config/QuerydslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.festimate.team.infra.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,15 @@ void findBestCandidateByPriority_success() {
.festival(festival)
.build();
ReflectionTestUtils.setField(applicant, "participantId", 1L);
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());

Participant target = Participant.builder()
.user(targetUser)
.typeResult(TypeResult.PHOTO)
.festival(festival)
.build();

when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(List.of(target));

var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
Expand All @@ -145,9 +146,10 @@ void findBestCandidate_prioritySecond_success() {
.festival(festival)
.build();
ReflectionTestUtils.setField(applicant, "participantId", 1L);
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());

// 1순위 PHOTO에 대상 없음
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(Collections.emptyList());

// 2순위 INFLUENCER에 대상 있음
Expand All @@ -157,7 +159,7 @@ void findBestCandidate_prioritySecond_success() {
.festival(festival)
.build();

when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(List.of(secondPriorityCandidate));

var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
Expand All @@ -176,16 +178,18 @@ void findBestCandidate_priorityThird_success() {

// 신청자
Participant applicant = mockParticipant(applicantUser, festival, TypeResult.INFLUENCER, 1L);
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());

// 3순위 NEWBIE에 대상 있음
Participant thirdPriorityCandidate = mockParticipant(thirdPriorityUser, festival, TypeResult.NEWBIE, 2L);

// 1순위 PHOTO, 2순위 INFLUENCER 대상 없음
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(Collections.emptyList());
when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(Collections.emptyList());
// 3순위 대상 있음
when(matchingRepository.findMatchingCandidates(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(List.of(thirdPriorityCandidate));

var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
Expand All @@ -206,13 +210,14 @@ void findBestCandidate_alreadyMatchedCandidate_empty() {
.festival(festival)
.build();
ReflectionTestUtils.setField(applicant, "participantId", 1L);
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());

// 대상이 존재하나 이미 매칭됨 (Repository에서 필터링 됨)
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(Collections.emptyList());
when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(Collections.emptyList());
when(matchingRepository.findMatchingCandidates(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(Collections.emptyList());

Optional<Participant> result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
Expand All @@ -231,9 +236,10 @@ void findBestCandidateByPriority_empty() {
.typeResult(TypeResult.INFLUENCER)
.festival(festival)
.build();
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());

when(matchingRepository.findMatchingCandidates(
applicant.getParticipantId(), TypeResult.PHOTO, Gender.MAN, festival.getFestivalId(), PageRequest.of(0, 1)
when(matchingRepository.findMatchingCandidatesDsl(
applicant.getParticipantId(), TypeResult.PHOTO, Gender.MAN, festival.getFestivalId(), PageRequest.of(0, 1), excludedIds
)).thenReturn(Collections.emptyList());

Optional<Participant> result = matchingService.findBestCandidateByPriority(
Expand Down Expand Up @@ -369,14 +375,15 @@ void findBestCandidate_multipleCandidates_returnsFirstOnly() {
User applicantUser = mockUser("신청자", Gender.MAN, 1L);
Festival festival = mockFestival(applicantUser, 1L, LocalDate.now().minusDays(1), LocalDate.now().plusDays(1));
Participant applicant = mockParticipant(applicantUser, festival, TypeResult.INFLUENCER, 1L);
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());

User candidate1 = mockUser("후보1", Gender.WOMAN, 2L);
User candidate2 = mockUser("후보2", Gender.WOMAN, 3L);
Participant p1 = mockParticipant(candidate1, festival, TypeResult.PHOTO, 2L);
Participant p2 = mockParticipant(candidate2, festival, TypeResult.PHOTO, 3L);

// 후보 2명 반환
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
.thenReturn(List.of(p1, p2));

// when
Expand Down
Loading