Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/186] 매칭 우선순위 API 구현 #189

Merged
merged 28 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e8d4ae1
:sparkles: [Feat] 매칭 API 클래스 구조 설계 및 계산 클래스 구현
rimi3226 Jan 22, 2025
c56a307
:sparkles: [Feat] 각 게임모드마다 달라지는 계산을 구현, MatchingPriorityCalculateSer…
rimi3226 Jan 22, 2025
374883e
:sparkles: [Feat] Mike ENUM 값 변경 및, 매칭 우선순위 계산 로직 작성
rimi3226 Jan 23, 2025
00b4ee8
:sparkles: [Feat] 빠른 대전 제외, MatchingPriorityCalculateService 매칭 타입, 게…
rimi3226 Jan 23, 2025
bb8e081
:sparkles: [Feat] 매칭 우선순위 API DTO 생성
rimi3226 Jan 25, 2025
b4f2e94
:sparkles: [Feat] 매칭 우선순위 API DTO 생성
rimi3226 Jan 25, 2025
14b3e7f
:sparkles: [Feat] GameStyle 변경하는 로직 MemberFacadeService가 아닌 MemberSer…
rimi3226 Jan 25, 2025
d6f06e7
:sparkles: [Feat] setGameStyle Service 수정
rimi3226 Jan 25, 2025
e2996c9
:sparkles: [Feat] 매칭 정보로 Member 업데이트
rimi3226 Jan 25, 2025
3e9ca64
:sparkles: [Feat] 매칭 우선순위 계산 Facade Service 구현
rimi3226 Jan 26, 2025
3c2b9db
:sparkles: [Feat] 제한 조건 추가
rimi3226 Jan 26, 2025
87d435b
:bug: [fix] 우선순위 API 구현 안되는거 해결
rimi3226 Jan 26, 2025
d2c828c
:bug: [fix] PENDING->Pending, MemberService, MemberGameStyleService 분리
rimi3226 Jan 26, 2025
52ce6d9
:sparkles: [Feat] 겜구매칭 DB 조회할 때, 조건 제외하고 조회하도록 수정
rimi3226 Feb 1, 2025
2ed159f
:sparkles: [Feat] 주석 추가
rimi3226 Feb 1, 2025
6cf005d
:sparkles: [Feat] 주석 추가
rimi3226 Feb 2, 2025
ea3641b
:white_check_mark: [Test] MatchingScoreCalculatorTest 테스트 구현
rimi3226 Feb 2, 2025
bca80d4
:white_check_mark: [Test] MatchingStrategyProcessorTest 테스트 구현
rimi3226 Feb 2, 2025
2b95339
:white_check_mark: [Test] 필드명 수정
rimi3226 Feb 2, 2025
ebba37a
:white_check_mark: [Test] MatchingService 매칭 리스트 조회 테스트 구현
rimi3226 Feb 4, 2025
e4749ff
:white_check_mark: [Test] MatchingService 매칭 게임모드, 매칭타입에 따른 우선순위 계산 테…
rimi3226 Feb 4, 2025
2caa5aa
:white_check_mark: [Test] MatchingServiceTest 대기 중인 매칭 리스트 조회 구현
rimi3226 Feb 4, 2025
721f2c7
:white_check_mark: [Test] MatchingServiceTest 매칭 기록 생성 테스트 구현
rimi3226 Feb 4, 2025
a069368
:sparkles: [Feat] 특정 사용자의 가장 최근 matchingRecord 조회
rimi3226 Feb 4, 2025
d4e7dc0
:white_check_mark: [Test] MatchingFacadeServiceTest 구현
rimi3226 Feb 4, 2025
e10de4b
:bug: [Fix] 불필요한 JPA 삭제
rimi3226 Feb 4, 2025
812aced
:white_check_mark: [Test] MatchingFacadeServiceTest 수정
rimi3226 Feb 4, 2025
93002d1
:recycle: [refactor] MatchingScoreCalculator static 변경
rimi3226 Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ public class Member extends BaseDateTimeEntity {
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<MemberGameStyle> memberGameStyleList = new ArrayList<>();

// 회원가입용 create
public static Member create(String email, String password, LoginType loginType, String gameName, String tag,
Tier tier, int gameRank, double winRate, int gameCount, boolean isAgree) {
int randomProfileImage = ThreadLocalRandom.current().nextInt(1, 9);
Expand All @@ -124,7 +123,6 @@ public static Member create(String email, String password, LoginType loginType,
.build();
}

// 회원가입용 Builder
@Builder
private Member(String email, String password, int profileImage, LoginType loginType, String gameName,
String tag, Tier tier, int gameRank, double winRate, int gameCount, boolean isAgree) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -25,13 +23,6 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(
indexes = {
@Index(name = "idx_matching_record_created_at_status_game_mode", columnList = "createdAt, status, " +
"gameMode"),
@Index(name = "idx_matching_record_member_id", columnList = "member_id")
}
)
public class MatchingRecord extends BaseDateTimeEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.util.Objects;

@Getter
@Builder
@ToString
public class PriorityValue {

Long memberId;
String matchingUuid;
int priorityValue;
private final Long memberId;
private final String matchingUuid;
private final int priorityValue;

public static PriorityValue of(Long memberId, String matchingUuid, int priorityValue) {
return PriorityValue.builder()
Expand All @@ -19,4 +23,23 @@ public static PriorityValue of(Long memberId, String matchingUuid, int priorityV
.build();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PriorityValue that = (PriorityValue) o;
return priorityValue == that.priorityValue &&
Objects.equals(memberId, that.memberId) &&
Objects.equals(matchingUuid, that.matchingUuid);
}

@Override
public int hashCode() {
return Objects.hash(memberId, matchingUuid, priorityValue);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import com.gamegoo.gamegoo_v2.game.dto.response.GameStyleResponse;
import com.gamegoo.gamegoo_v2.matching.domain.GameMode;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
@EqualsAndHashCode
public class MatchingMemberInfoResponse {

Long memberId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import com.gamegoo.gamegoo_v2.account.member.domain.Member;
import com.gamegoo.gamegoo_v2.matching.dto.PriorityValue;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
@EqualsAndHashCode
public class PriorityListResponse {

List<PriorityValue> myPriorityList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,19 @@

import com.gamegoo.gamegoo_v2.matching.domain.GameMode;
import com.gamegoo.gamegoo_v2.matching.domain.MatchingRecord;
import com.gamegoo.gamegoo_v2.matching.domain.MatchingStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.List;

public interface MatchingRecordRepository extends JpaRepository<MatchingRecord, String> {
public interface MatchingRecordRepository extends JpaRepository<MatchingRecord, String>,
MatchingRecordRepositoryCustom {

@Query(value = """
SELECT m
FROM MatchingRecord m
WHERE m.createdAt > :createdAt
AND m.status = :status
AND m.gameMode = :gameMode
GROUP BY m.member.id
""")
List<MatchingRecord> findMatchingRecordsWithGroupBy(
@Param("createdAt") LocalDateTime createdAt,
@Param("status") MatchingStatus status,
@Param("gameMode") GameMode gameMode
);
/**
* 5분 이내, 특정 게임 모드, PENDING 상태의 매칭 레코드 조회
*/
default List<MatchingRecord> findRecentValidMatchingRecords(GameMode gameMode) {
return findValidMatchingRecords(LocalDateTime.now().minusMinutes(5), gameMode);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.gamegoo.gamegoo_v2.matching.repository;

import com.gamegoo.gamegoo_v2.account.member.domain.Member;
import com.gamegoo.gamegoo_v2.matching.domain.GameMode;
import com.gamegoo.gamegoo_v2.matching.domain.MatchingRecord;

import java.time.LocalDateTime;
import java.util.List;

public interface MatchingRecordRepositoryCustom {

/**
* 매칭 가능한 유효한 레코드 조회 (5분 이내, 특정 게임 모드, PENDING 상태)
*
* @param createdAt 5분 전 시간 기준
* @param gameMode 게임 모드
* @return 매칭 가능한 레코드 리스트
*/
List<MatchingRecord> findValidMatchingRecords(LocalDateTime createdAt, GameMode gameMode);

/**
* 가장 최근 기록 불러오기
*
* @param member 사용자
* @return 사용자의 가장 최근 매칭 기록
*/
MatchingRecord findLatestByMember(Member member);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.gamegoo.gamegoo_v2.matching.repository;

import com.gamegoo.gamegoo_v2.account.member.domain.Member;
import com.gamegoo.gamegoo_v2.matching.domain.GameMode;
import com.gamegoo.gamegoo_v2.account.member.domain.Position;
import com.gamegoo.gamegoo_v2.matching.domain.MatchingRecord;
import com.gamegoo.gamegoo_v2.matching.domain.MatchingStatus;
import com.gamegoo.gamegoo_v2.account.member.domain.Tier;
import com.gamegoo.gamegoo_v2.matching.domain.QMatchingRecord;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.EnumPath;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.JPAExpressions;
import lombok.RequiredArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

import static com.gamegoo.gamegoo_v2.matching.domain.QMatchingRecord.matchingRecord;

@RequiredArgsConstructor
public class MatchingRecordRepositoryCustomImpl implements MatchingRecordRepositoryCustom {

private final JPAQueryFactory queryFactory;

@Override
public List<MatchingRecord> findValidMatchingRecords(LocalDateTime createdAt, GameMode gameMode) {
return queryFactory.selectFrom(matchingRecord)
.where(
matchingRecord.createdAt.gt(createdAt), // 5분 이내
matchingRecord.status.eq(MatchingStatus.PENDING), // 상태 PENDING
matchingRecord.gameMode.eq(gameMode), // 특정 게임 모드
existsValidMatchSubquery(), // 유효한 매칭만 가져오기
applyGameModeFilter(gameMode) // 게임모드별 추가 필터 적용 (SOLO, FREE)
)
.orderBy(matchingRecord.member.id.asc(), matchingRecord.createdAt.desc()) // 최신순 정렬
.fetch();
}

@Override
public MatchingRecord findLatestByMember(Member member) {
QMatchingRecord matchingRecord = QMatchingRecord.matchingRecord;

return queryFactory
.selectFrom(matchingRecord)
.where(matchingRecord.member.eq(member))
.orderBy(matchingRecord.createdAt.desc())
.limit(1)
.fetchOne();
}

/**
* 게임 모드에 따른 추가 필터 적용
*/
private BooleanExpression applyGameModeFilter(GameMode gameMode) {
return switch (gameMode) {
case SOLO -> validateSoloRankFilter(); // 개인 랭크 티어 검증
case FREE -> validateFreeRankFilter(); // 자유 랭크 티어 검증
default -> Expressions.TRUE; // 다른 게임 모드는 필터 없음
};
}

/**
* SOLO 모드 - 개인 랭크 제한 검증 (validateSoloRankRange 적용)
*/
private BooleanExpression validateSoloRankFilter() {
return validateSoloRankRange(matchingRecord.soloTier);
}

/**
* FREE 모드 - 자유 랭크 제한 검증
*/
private BooleanExpression validateFreeRankFilter() {
return matchingRecord.freeTier.in(Tier.IRON, Tier.BRONZE, Tier.SILVER, Tier.GOLD)
.and(matchingRecord.freeTier.notIn(Tier.EMERALD, Tier.DIAMOND, Tier.MASTER, Tier.GRANDMASTER,
Tier.CHALLENGER));
}


/**
* 매칭 유효성 검사 서브쿼리
*/
private BooleanExpression existsValidMatchSubquery() {
QMatchingRecord otherRecord = new QMatchingRecord("otherRecord");

return JPAExpressions.selectOne()
.from(otherRecord)
.where(
otherRecord.member.id.ne(matchingRecord.member.id), // 다른 멤버와의 매칭
isValidMatchingPosition(matchingRecord, otherRecord) // 포지션 조건 체크
)
.exists();
}

/**
* 매칭 가능한 포지션 조건 ]
*/
private BooleanExpression isValidMatchingPosition(QMatchingRecord myRecord, QMatchingRecord otherRecord) {
BooleanExpression condition1 = myRecord.mainPosition.ne(Position.ANY)
.and(otherRecord.subPosition.ne(Position.ANY))
.and(myRecord.mainPosition.eq(otherRecord.subPosition)
.or(otherRecord.mainPosition.eq(myRecord.subPosition)))
.and(myRecord.wantPosition.eq(otherRecord.subPosition));

BooleanExpression condition2 = myRecord.subPosition.ne(Position.ANY)
.and(otherRecord.wantPosition.ne(Position.ANY))
.and(otherRecord.wantPosition.eq(myRecord.subPosition))
.and(otherRecord.mainPosition.eq(myRecord.subPosition)
.or(myRecord.mainPosition.eq(otherRecord.subPosition)));

BooleanExpression condition3 = myRecord.mainPosition.ne(Position.ANY)
.and(myRecord.subPosition.ne(Position.ANY))
.and(otherRecord.mainPosition.ne(Position.ANY))
.and(otherRecord.subPosition.ne(Position.ANY))
.and(myRecord.mainPosition.eq(otherRecord.mainPosition)
.or(myRecord.mainPosition.eq(otherRecord.subPosition)))
.and(myRecord.subPosition.eq(otherRecord.mainPosition)
.or(myRecord.subPosition.eq(otherRecord.subPosition)))
.not();

return condition1.or(condition2).or(condition3);
}


/**
* 개인 랭크 제한 검증 (SOLO 모드 전용) - QueryDSL에서 적용 가능하도록 수정
*/
private BooleanExpression validateSoloRankRange(EnumPath<Tier> tierPath) {
return tierPath.eq(Tier.IRON).or(tierPath.eq(Tier.BRONZE)).and(matchingRecord.soloTier.in(Tier.IRON,
Tier.BRONZE, Tier.SILVER))
.or(tierPath.eq(Tier.SILVER).and(matchingRecord.soloTier.in(Tier.IRON, Tier.BRONZE, Tier.SILVER,
Tier.GOLD)))
.or(tierPath.eq(Tier.GOLD).and(matchingRecord.soloTier.in(Tier.SILVER, Tier.GOLD, Tier.PLATINUM)))
.or(tierPath.eq(Tier.PLATINUM).and(matchingRecord.soloTier.in(Tier.GOLD, Tier.PLATINUM, Tier.EMERALD)))
.or(tierPath.eq(Tier.EMERALD).and(matchingRecord.soloTier.in(Tier.PLATINUM, Tier.EMERALD,
Tier.DIAMOND)))
.or(tierPath.eq(Tier.DIAMOND).and(matchingRecord.soloTier.in(Tier.EMERALD, Tier.DIAMOND)))
.or(Expressions.FALSE); // 마스터 이상 필터 적용 안 함
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ public class MatchingFacadeService {
private final MemberGameStyleService memberGameStyleService;

/**
* 매칭 우선순위 계산 및 기록 저장 API
* 매칭 우선순위 계산 및 DB 저장
*
* @param memberId 회원 ID
* @param request 회원 정보
* @return 매칭 정보
*/
@Transactional
public PriorityListResponse calculatePriorityAndRecording(Long memberId, InitializingMatchingRequest request) {
Expand Down
Loading