Skip to content

Commit

Permalink
[BE] 방 매칭 실패 시 매칭 실패 원인을 전달하는 기능 구현(#562) (#575)
Browse files Browse the repository at this point in the history
* feat: 반환된 예외를 통해 실패 원인을 찾는 기능 구현

* feat: 실패한 매칭 정보를 저장하는 엔티티 구현

* feat: 매칭을 실패했을 경우 실패한 매칭 정보를 저장하는 기능 구현

* refactor: 클래스명 변경

* feat: 반환된 예외를 통해 매칭 실패 원인에 대한 메세지를 얻는 기능 구현

* refactor: FailedMatching 생성자 변경

* feat: 매칭 실패한 방을 조회시, 실패 원인을 같이 전달하는 기능 구현

* refactor: 맵 구현체 변경

* refactor: 피드백 반영

---------

Co-authored-by: gyungchan Jo <[email protected]>
  • Loading branch information
github-actions[bot] and jcoding-play authored Oct 12, 2024
1 parent 5f5466f commit 236675e
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 16 deletions.
32 changes: 32 additions & 0 deletions backend/src/main/java/corea/matchresult/domain/FailedMatching.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package corea.matchresult.domain;

import corea.exception.ExceptionType;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class FailedMatching {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private long roomId;

@Enumerated(EnumType.STRING)
private MatchingFailedReason reason;

public FailedMatching(long roomId, ExceptionType exceptionType) {
this(null, roomId, MatchingFailedReason.from(exceptionType));
}

public String getMatchingFailedReason() {
return reason.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package corea.matchresult.domain;

import corea.exception.ExceptionType;
import lombok.Getter;

import java.util.Arrays;

@Getter
public enum MatchingFailedReason {

ROOM_NOT_FOUND(ExceptionType.ROOM_NOT_FOUND, "기존에 존재하던 방이 방장의 삭제로 인해 더 이상 유효하지 않아 매칭이 진행되지 않았습니다."),
ROOM_STATUS_INVALID(ExceptionType.ROOM_STATUS_INVALID, "방이 이미 매칭 중이거나, 매칭이 완료되어 더 이상 매칭을 진행할 수 없는 상태입니다."),

PARTICIPANT_SIZE_LACK(ExceptionType.PARTICIPANT_SIZE_LACK, "방의 최소 참여 인원보다 참가자가 부족하여 매칭이 진행되지 않았습니다."),
PARTICIPANT_SIZE_LACK_DUE_TO_PULL_REQUEST(ExceptionType.PARTICIPANT_SIZE_LACK_DUE_TO_PULL_REQUEST, "참가자의 수는 최소 인원을 충족하였지만, 일부 참가자가 pull request를 제출하지 않아 매칭이 진행되지 않았습니다."),

AUTOMATIC_MATCHING_NOT_FOUND(ExceptionType.AUTOMATIC_MATCHING_NOT_FOUND, "해당 방에 대해 예약된 자동 매칭 시간이 존재하지 않거나 설정되지 않아 매칭이 진행되지 않았습니다."),

UNKNOWN(null, "매칭 과정에서 오류가 발생하여 매칭이 진행되지 않았습니다. 문제가 지속될 경우 관리자에게 문의하세요."),
;

private final ExceptionType exceptionType;
private final String message;

MatchingFailedReason(ExceptionType exceptionType, String message) {
this.exceptionType = exceptionType;
this.message = message;
}

public static MatchingFailedReason from(ExceptionType exceptionType) {
return Arrays.stream(values())
.filter(reason -> reason.isTypeMatching(exceptionType))
.findAny()
.orElse(UNKNOWN);
}

private boolean isTypeMatching(ExceptionType exceptionType) {
return this.exceptionType == exceptionType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package corea.matchresult.repository;

import corea.matchresult.domain.FailedMatching;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface FailedMatchingRepository extends JpaRepository<FailedMatching, Long> {

Optional<FailedMatching> findByRoomId(long roomId);
}
54 changes: 52 additions & 2 deletions backend/src/main/java/corea/room/dto/RoomResponse.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package corea.room.dto;

import corea.matchresult.domain.FailedMatching;
import corea.member.domain.MemberRole;
import corea.participation.domain.Participation;
import corea.participation.domain.ParticipationStatus;
import corea.room.domain.Room;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -52,9 +54,14 @@ public record RoomResponse(@Schema(description = "방 아이디", example = "1")
MemberRole memberRole,

@Schema(description = "방 상태", example = "OPEN")
String roomStatus
String roomStatus,

@Schema(description = "매칭 실패 원인 메세지 제공", example = "참여 인원이 부족하여 매칭을 진행할 수 없습니다.")
String message
) {

private static final String DEFAULT_MESSAGE = "";

public static RoomResponse from(Room room) {
return RoomResponse.of(room, MemberRole.BOTH, ParticipationStatus.NOT_PARTICIPATED);
}
Expand All @@ -76,7 +83,50 @@ public static RoomResponse of(Room room, MemberRole role, ParticipationStatus pa
room.getReviewDeadline(),
participationStatus,
role,
room.getRoomStatus()
room.getRoomStatus(),
DEFAULT_MESSAGE
);
}

public static RoomResponse of(Room room, Participation participation) {
return new RoomResponse(
room.getId(),
room.getTitle(),
room.getContent(),
room.getManagerName(),
room.getRepositoryLink(),
room.getThumbnailLink(),
room.getMatchingSize(),
room.getKeyword(),
room.getCurrentParticipantsSize(),
room.getLimitedParticipantsSize(),
room.getRecruitmentDeadline(),
room.getReviewDeadline(),
participation.getStatus(),
participation.getMemberRole(),
room.getRoomStatus(),
DEFAULT_MESSAGE
);
}

public static RoomResponse of(Room room, Participation participation, FailedMatching failedMatching) {
return new RoomResponse(
room.getId(),
room.getTitle(),
room.getContent(),
room.getManagerName(),
room.getRepositoryLink(),
room.getThumbnailLink(),
room.getMatchingSize(),
room.getKeyword(),
room.getCurrentParticipantsSize(),
room.getLimitedParticipantsSize(),
room.getRecruitmentDeadline(),
room.getReviewDeadline(),
participation.getStatus(),
participation.getMemberRole(),
room.getRoomStatus(),
failedMatching.getMatchingFailedReason()
);
}
}
23 changes: 15 additions & 8 deletions backend/src/main/java/corea/room/service/RoomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import corea.exception.CoreaException;
import corea.exception.ExceptionType;
import corea.matchresult.repository.FailedMatchingRepository;
import corea.matchresult.repository.MatchResultRepository;
import corea.member.domain.Member;
import corea.member.domain.MemberRole;
import corea.member.repository.MemberRepository;
import corea.participation.domain.Participation;
import corea.participation.domain.ParticipationStatus;
import corea.participation.repository.ParticipationRepository;
import corea.room.domain.Room;
import corea.room.domain.RoomClassification;
Expand All @@ -29,8 +31,6 @@
import java.util.Collections;
import java.util.List;

import static corea.participation.domain.ParticipationStatus.*;

@Slf4j
@Service
@RequiredArgsConstructor
Expand All @@ -46,6 +46,7 @@ public class RoomService {
private final MemberRepository memberRepository;
private final MatchResultRepository matchResultRepository;
private final ParticipationRepository participationRepository;
private final FailedMatchingRepository failedMatchingRepository;
private final AutomaticMatchingRepository automaticMatchingRepository;
private final AutomaticUpdateRepository automaticUpdateRepository;

Expand All @@ -62,7 +63,7 @@ public RoomResponse create(long memberId, RoomCreateRequest request) {
automaticMatchingRepository.save(new AutomaticMatching(room.getId(), request.recruitmentDeadline()));
automaticUpdateRepository.save(new AutomaticUpdate(room.getId(), request.reviewDeadline()));

return RoomResponse.of(room, participation.getMemberRole(), MANAGER);
return RoomResponse.of(room, participation.getMemberRole(), ParticipationStatus.MANAGER);
}

private void validateDeadLine(LocalDateTime recruitmentDeadline, LocalDateTime reviewDeadline) {
Expand All @@ -84,8 +85,14 @@ public RoomResponse findOne(long roomId, long memberId) {
Room room = getRoom(roomId);

return participationRepository.findByRoomIdAndMemberId(roomId, memberId)
.map(participation -> RoomResponse.of(room, participation.getMemberRole(), participation.getStatus()))
.orElseGet(() -> RoomResponse.of(room, MemberRole.NONE, NOT_PARTICIPATED));
.map(participation -> createRoomResponseWithParticipation(room, participation))
.orElseGet(() -> RoomResponse.of(room, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED));
}

private RoomResponse createRoomResponseWithParticipation(Room room, Participation participation) {
return failedMatchingRepository.findByRoomId(room.getId())
.map(failedMatching -> RoomResponse.of(room, participation, failedMatching))
.orElseGet(() -> RoomResponse.of(room, participation));
}

public RoomResponses findParticipatedRooms(long memberId) {
Expand All @@ -95,7 +102,7 @@ public RoomResponses findParticipatedRooms(long memberId) {
.toList();

List<Room> rooms = roomRepository.findAllByIdInOrderByReviewDeadlineAsc(roomIds);
return RoomResponses.of(rooms, MemberRole.NONE, PARTICIPATED, true, 0);
return RoomResponses.of(rooms, MemberRole.NONE, ParticipationStatus.PARTICIPATED, true, 0);
}

public RoomResponses findRoomsWithRoomStatus(long memberId, int pageNumber, String expression, RoomStatus roomStatus) {
Expand All @@ -108,10 +115,10 @@ private RoomResponses getRoomResponses(long memberId, int pageNumber, RoomClassi

if (classification.isAll()) {
Page<Room> roomsWithPage = roomRepository.findAllByMemberAndStatus(memberId, status, pageRequest);
return RoomResponses.of(roomsWithPage, MemberRole.NONE, NOT_PARTICIPATED, pageNumber);
return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber);
}
Page<Room> roomsWithPage = roomRepository.findAllByMemberAndClassificationAndStatus(memberId, classification, status, pageRequest);
return RoomResponses.of(roomsWithPage, MemberRole.NONE, NOT_PARTICIPATED, pageNumber);
return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import corea.matching.domain.PullRequestInfo;
import corea.matching.service.MatchingService;
import corea.matching.service.PullRequestProvider;
import corea.matchresult.domain.FailedMatching;
import corea.matchresult.repository.FailedMatchingRepository;
import corea.room.domain.Room;
import corea.room.repository.RoomRepository;
import corea.scheduler.domain.AutomaticMatching;
Expand All @@ -25,6 +27,7 @@ public class AutomaticMatchingExecutor {
private final MatchingService matchingService;
private final PullRequestProvider pullRequestProvider;
private final RoomRepository roomRepository;
private final FailedMatchingRepository failedMatchingRepository;
private final AutomaticMatchingRepository automaticMatchingRepository;

@Async
Expand All @@ -39,7 +42,7 @@ public void execute(long roomId) {
});
} catch (CoreaException e) {
log.warn("매칭 실행 중 에러 발생: {}", e.getMessage(), e);
updateRoomStatusToFail(roomId);
recordMatchingFailure(roomId, e.getExceptionType());
}
}

Expand All @@ -53,16 +56,26 @@ private void startMatching(long roomId) {
automaticMatching.updateStatusToDone();
}

private void updateRoomStatusToFail(long roomId) {
private void recordMatchingFailure(long roomId, ExceptionType exceptionType) {
//TODO: 위와 동일
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.execute(status -> {
Room room = getRoom(roomId);
room.updateStatusToFail();
updateRoomStatusToFail(roomId);
saveFailedMatching(roomId, exceptionType);
return null;
});
}

private void updateRoomStatusToFail(long roomId) {
Room room = getRoom(roomId);
room.updateStatusToFail();
}

private void saveFailedMatching(long roomId, ExceptionType exceptionType) {
FailedMatching failedMatching = new FailedMatching(roomId, exceptionType);
failedMatchingRepository.save(failedMatching);
}

private Room getRoom(long roomId) {
return roomRepository.findById(roomId)
.orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package corea.matchresult.domain;

import corea.exception.ExceptionType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class MatchingFailedReasonTest {

@ParameterizedTest
@CsvSource(value = {"ROOM_NOT_FOUND, ROOM_NOT_FOUND", "AUTOMATIC_MATCHING_NOT_FOUND, AUTOMATIC_MATCHING_NOT_FOUND"})
@DisplayName("ExceptionType을 통해 매칭이 실패한 이유를 찾을 수 있다.")
void from(ExceptionType exceptionType, MatchingFailedReason expected) {
MatchingFailedReason actual = MatchingFailedReason.from(exceptionType);

assertThat(actual).isEqualTo(expected);
}

@Test
@DisplayName("ExceptionType을 통해 매칭 실패 이유를 찾을 수 없다면 UNKNOWN 반환한다.")
void unknownReason() {
MatchingFailedReason reason = MatchingFailedReason.from(ExceptionType.ALREADY_COMPLETED_FEEDBACK);

assertThat(reason).isEqualTo(MatchingFailedReason.UNKNOWN);
}
}
20 changes: 20 additions & 0 deletions backend/src/test/java/corea/room/service/RoomServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import corea.fixture.MatchResultFixture;
import corea.fixture.MemberFixture;
import corea.fixture.RoomFixture;
import corea.matchresult.domain.FailedMatching;
import corea.matchresult.repository.FailedMatchingRepository;
import corea.matchresult.repository.MatchResultRepository;
import corea.member.domain.Member;
import corea.member.domain.MemberRole;
Expand Down Expand Up @@ -59,6 +61,9 @@ class RoomServiceTest {
@Autowired
private ParticipationRepository participationRepository;

@Autowired
private FailedMatchingRepository failedMatchingRepository;

@Test
@DisplayName("방을 생성할 수 있다.")
void create() {
Expand Down Expand Up @@ -135,6 +140,21 @@ void findOne_not_participated() {
assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.NOT_PARTICIPATED);
}

@Test
@DisplayName("매칭을 실패한 방을 조회할 때 실패한 원인에 대해 알 수 있다.")
void findOne_with_matching_fail() {
Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON());
Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager));

Member member = memberRepository.save(MemberFixture.MEMBER_PORORO());
participationRepository.save(new Participation(room, member, MemberRole.BOTH, room.getMatchingSize()));

failedMatchingRepository.save(new FailedMatching(room.getId(), ExceptionType.PARTICIPANT_SIZE_LACK));
RoomResponse response = roomService.findOne(room.getId(), member.getId());

assertThat(response.message()).isEqualTo("방의 최소 참여 인원보다 참가자가 부족하여 매칭이 진행되지 않았습니다.");
}

@Test
@DisplayName("현재 로그인한 멤버가 참여 중인 방을 리뷰 마감일이 임박한 순으로 보여준다.")
void findParticipatedRooms() {
Expand Down
Loading

0 comments on commit 236675e

Please sign in to comment.