From 023aa601ea6cbd8b531bda908cec689b5441d56f Mon Sep 17 00:00:00 2001 From: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:56:38 +0900 Subject: [PATCH 01/31] =?UTF-8?q?feat:=20Qualifier=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/corea/global/jpa/DataSourceConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/corea/global/jpa/DataSourceConfig.java b/backend/src/main/java/corea/global/jpa/DataSourceConfig.java index 586c5a155..b3ad225ba 100644 --- a/backend/src/main/java/corea/global/jpa/DataSourceConfig.java +++ b/backend/src/main/java/corea/global/jpa/DataSourceConfig.java @@ -2,6 +2,7 @@ import com.zaxxer.hikari.HikariDataSource; import jakarta.persistence.EntityManagerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.*; @@ -37,7 +38,8 @@ public DataSource readDataSource() { // 읽기 모드인지 여부로 DataSource를 분기 처리 @Bean @DependsOn({"writeDataSource", "readDataSource"}) - public DataSource routeDataSource(DataSource writeDataSource, DataSource readDataSource) { + public DataSource routeDataSource(@Qualifier("writeDataSource") DataSource writeDataSource, + @Qualifier("readDataSource") DataSource readDataSource) { DataSourceRouter dataSourceRouter = new DataSourceRouter(); HashMap dataSourceMap = new HashMap<>(); @@ -57,7 +59,6 @@ public DataSource dataSource(DataSource routeDataSource) { } @Bean - @Primary public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } From 236675ecdcc604624c4ae051a3d8eb92115d78ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 05:13:17 +0900 Subject: [PATCH 02/31] =?UTF-8?q?[BE]=20=EB=B0=A9=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EB=A7=A4=EC=B9=AD=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=9B=90=EC=9D=B8=EC=9D=84=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84(#5?= =?UTF-8?q?62)=20(#575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 반환된 예외를 통해 실패 원인을 찾는 기능 구현 * feat: 실패한 매칭 정보를 저장하는 엔티티 구현 * feat: 매칭을 실패했을 경우 실패한 매칭 정보를 저장하는 기능 구현 * refactor: 클래스명 변경 * feat: 반환된 예외를 통해 매칭 실패 원인에 대한 메세지를 얻는 기능 구현 * refactor: FailedMatching 생성자 변경 * feat: 매칭 실패한 방을 조회시, 실패 원인을 같이 전달하는 기능 구현 * refactor: 맵 구현체 변경 * refactor: 피드백 반영 --------- Co-authored-by: gyungchan Jo --- .../matchresult/domain/FailedMatching.java | 32 +++++++++++ .../domain/MatchingFailedReason.java | 40 ++++++++++++++ .../repository/FailedMatchingRepository.java | 11 ++++ .../java/corea/room/dto/RoomResponse.java | 54 ++++++++++++++++++- .../java/corea/room/service/RoomService.java | 23 +++++--- .../service/AutomaticMatchingExecutor.java | 21 ++++++-- .../domain/MatchingFailedReasonTest.java | 30 +++++++++++ .../corea/room/service/RoomServiceTest.java | 20 +++++++ .../AutomaticMatchingExecutorTest.java | 16 +++++- .../service/AutomaticMatchingServiceTest.java | 3 +- 10 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 backend/src/main/java/corea/matchresult/domain/FailedMatching.java create mode 100644 backend/src/main/java/corea/matchresult/domain/MatchingFailedReason.java create mode 100644 backend/src/main/java/corea/matchresult/repository/FailedMatchingRepository.java create mode 100644 backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java diff --git a/backend/src/main/java/corea/matchresult/domain/FailedMatching.java b/backend/src/main/java/corea/matchresult/domain/FailedMatching.java new file mode 100644 index 000000000..a10ab8388 --- /dev/null +++ b/backend/src/main/java/corea/matchresult/domain/FailedMatching.java @@ -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(); + } +} diff --git a/backend/src/main/java/corea/matchresult/domain/MatchingFailedReason.java b/backend/src/main/java/corea/matchresult/domain/MatchingFailedReason.java new file mode 100644 index 000000000..54923717f --- /dev/null +++ b/backend/src/main/java/corea/matchresult/domain/MatchingFailedReason.java @@ -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; + } +} diff --git a/backend/src/main/java/corea/matchresult/repository/FailedMatchingRepository.java b/backend/src/main/java/corea/matchresult/repository/FailedMatchingRepository.java new file mode 100644 index 000000000..4d329e86a --- /dev/null +++ b/backend/src/main/java/corea/matchresult/repository/FailedMatchingRepository.java @@ -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 { + + Optional findByRoomId(long roomId); +} diff --git a/backend/src/main/java/corea/room/dto/RoomResponse.java b/backend/src/main/java/corea/room/dto/RoomResponse.java index f285e6ddb..3d2d4fcad 100644 --- a/backend/src/main/java/corea/room/dto/RoomResponse.java +++ b/backend/src/main/java/corea/room/dto/RoomResponse.java @@ -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; @@ -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); } @@ -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() ); } } diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index 67073342f..e7f081d4c 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -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; @@ -29,8 +31,6 @@ import java.util.Collections; import java.util.List; -import static corea.participation.domain.ParticipationStatus.*; - @Slf4j @Service @RequiredArgsConstructor @@ -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; @@ -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) { @@ -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) { @@ -95,7 +102,7 @@ public RoomResponses findParticipatedRooms(long memberId) { .toList(); List 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) { @@ -108,10 +115,10 @@ private RoomResponses getRoomResponses(long memberId, int pageNumber, RoomClassi if (classification.isAll()) { Page 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 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 diff --git a/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java b/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java index 6d91059c8..d90f2d49a 100644 --- a/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java +++ b/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java @@ -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; @@ -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 @@ -39,7 +42,7 @@ public void execute(long roomId) { }); } catch (CoreaException e) { log.warn("매칭 실행 중 에러 발생: {}", e.getMessage(), e); - updateRoomStatusToFail(roomId); + recordMatchingFailure(roomId, e.getExceptionType()); } } @@ -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)); diff --git a/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java b/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java new file mode 100644 index 000000000..e323bf04c --- /dev/null +++ b/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java @@ -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); + } +} diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index 5e1a156fb..ef26c0c86 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -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; @@ -59,6 +61,9 @@ class RoomServiceTest { @Autowired private ParticipationRepository participationRepository; + @Autowired + private FailedMatchingRepository failedMatchingRepository; + @Test @DisplayName("방을 생성할 수 있다.") void create() { @@ -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() { diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java index 27a10362a..bd84e9850 100644 --- a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java +++ b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java @@ -9,6 +9,7 @@ import corea.matching.infrastructure.dto.PullRequestResponse; import corea.matching.service.PullRequestProvider; import corea.matchresult.domain.MatchResult; +import corea.matchresult.repository.FailedMatchingRepository; import corea.matchresult.repository.MatchResultRepository; import corea.member.domain.Member; import corea.member.domain.MemberRole; @@ -58,6 +59,9 @@ class AutomaticMatchingExecutorTest { @Autowired private ParticipationRepository participationRepository; + @Autowired + private FailedMatchingRepository failedMatchingRepository; + @MockBean private PullRequestProvider pullRequestProvider; @@ -121,11 +125,21 @@ void execute() { @Transactional @Test @DisplayName("매칭 시도 중 예외가 발생했다면 방 상태를 FAIL로 변경한다.") - void matchFail() { + void updateRoomStatusToFail() { AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(emptyParticipantRoom.getId(), emptyParticipantRoom.getRecruitmentDeadline())); automaticMatchingExecutor.execute(automaticMatching.getRoomId()); assertThat(emptyParticipantRoom.getStatus()).isEqualTo(RoomStatus.FAIL); } + + @Test + @DisplayName("매칭 시도 중 예외가 발생했다면 매칭을 실패한 방을 저장한다.") + void saveFailedMatching() { + AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(emptyParticipantRoom.getId(), emptyParticipantRoom.getRecruitmentDeadline())); + + automaticMatchingExecutor.execute(automaticMatching.getRoomId()); + + assertThat(failedMatchingRepository.findByRoomId(emptyParticipantRoom.getId())).isNotEmpty(); + } } diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java index fdad52f75..80ffc87ec 100644 --- a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java +++ b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java @@ -107,6 +107,7 @@ private RoomResponse getRoomResponse(LocalDateTime recruitmentDeadline) { LocalDateTime.now().plusDays(3), ParticipationStatus.PARTICIPATED, MemberRole.NONE, - "OPEN"); + "OPEN", + ""); } } From 82d5e48bb3a7b2957bc140cfae14dd94887226f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 05:14:45 +0900 Subject: [PATCH 03/31] =?UTF-8?q?[BE]=20=EB=B0=A9=20=EC=A2=85=EB=A3=8C=20?= =?UTF-8?q?=EC=8B=9C,=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B2=84=ED=8A=BC=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=EC=95=88=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84(#579)=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 방 종료 후 코드 리뷰 완료 버튼 검증 로직 구현 * fix: 잘못된 요청값 수정 (#566) Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> * refactor: 리뷰 완료 요청 검증 로직 변경 --------- Co-authored-by: gyungchan Jo Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> --- .../corea/review/service/ReviewService.java | 21 +++++++++++++++++-- .../src/main/java/corea/room/domain/Room.java | 4 ++++ .../java/corea/room/domain/RoomStatus.java | 4 ++++ .../review/service/ReviewServiceTest.java | 18 ++++++++++++++-- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/corea/review/service/ReviewService.java b/backend/src/main/java/corea/review/service/ReviewService.java index ba8e2e4f9..1831d963e 100644 --- a/backend/src/main/java/corea/review/service/ReviewService.java +++ b/backend/src/main/java/corea/review/service/ReviewService.java @@ -8,6 +8,8 @@ import corea.matchresult.repository.MatchResultRepository; import corea.member.domain.Member; import corea.member.repository.MemberRepository; +import corea.room.domain.Room; +import corea.room.repository.RoomRepository; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,12 +25,16 @@ public class ReviewService { private static final Logger log = LogManager.getLogger(ReviewService.class); - private final MatchResultRepository matchResultRepository; - private final MemberRepository memberRepository; private final GithubOAuthProvider githubOAuthProvider; + private final RoomRepository roomRepository; + private final MemberRepository memberRepository; + private final MatchResultRepository matchResultRepository; @Transactional public void completeReview(long roomId, long reviewerId, long revieweeId) { + Room room = getRoom(roomId); + validateRoomStatus(room); + MatchResult matchResult = getMatchResult(roomId, reviewerId, revieweeId); matchResult.reviewComplete(); updateReviewLink(matchResult, reviewerId); @@ -36,6 +42,17 @@ public void completeReview(long roomId, long reviewerId, long revieweeId) { log.info("리뷰 완료[{매칭 ID({}), 리뷰어 ID({}, 리뷰이 ID({})", matchResult.getId(), reviewerId, revieweeId); } + private Room getRoom(long roomId) { + return roomRepository.findById(roomId) + .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND, String.format("해당 Id의 방이 없습니다. 입력된 Id=%d", roomId))); + } + + private void validateRoomStatus(Room room) { + if (room.isNotProgress()) { + throw new CoreaException(ExceptionType.ROOM_STATUS_INVALID); + } + } + private void updateReviewLink(MatchResult matchResult, long reviewerId) { Member reviewer = memberRepository.findById(reviewerId) .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); diff --git a/backend/src/main/java/corea/room/domain/Room.java b/backend/src/main/java/corea/room/domain/Room.java index 83f240711..e18773ab0 100644 --- a/backend/src/main/java/corea/room/domain/Room.java +++ b/backend/src/main/java/corea/room/domain/Room.java @@ -111,6 +111,10 @@ public boolean isClosed() { return status.isClosed(); } + public boolean isNotProgress() { + return status.isNotProgress(); + } + public boolean isNotMatchingManager(long memberId) { return manager.isNotMatchingId(memberId); } diff --git a/backend/src/main/java/corea/room/domain/RoomStatus.java b/backend/src/main/java/corea/room/domain/RoomStatus.java index f5de9b08d..d67abd751 100644 --- a/backend/src/main/java/corea/room/domain/RoomStatus.java +++ b/backend/src/main/java/corea/room/domain/RoomStatus.java @@ -12,6 +12,10 @@ public boolean isNotOpened() { return this != OPEN; } + public boolean isNotProgress() { + return this != PROGRESS; + } + public String getStatus() { return this.name(); } diff --git a/backend/src/test/java/corea/review/service/ReviewServiceTest.java b/backend/src/test/java/corea/review/service/ReviewServiceTest.java index 204c5d951..2740bbc8d 100644 --- a/backend/src/test/java/corea/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/corea/review/service/ReviewServiceTest.java @@ -52,7 +52,7 @@ class ReviewServiceTest { void completeReview() { Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_PROGRESS(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); MatchResult matchResult = matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), reviewer, reviewee)); when(githubOAuthProvider.getPullRequestReview(anyString())) @@ -77,7 +77,7 @@ void completeReview() { void notCompleteReview() { Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_PROGRESS(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), reviewer, reviewee)); when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[]{}); @@ -88,6 +88,20 @@ void notCompleteReview() { .isEqualTo(ExceptionType.NOT_COMPLETE_GITHUB_REVIEW); } + @Test + @DisplayName("방이 종료되고 코드 리뷰 완료 버튼을 누르면 예외가 발생한다.") + void completeReviewAfterRoomClosed() { + Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); + matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), reviewer, reviewee)); + + assertThatThrownBy(() -> reviewService.completeReview(room.getId(), reviewer.getId(), reviewee.getId())) + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.ROOM_STATUS_INVALID); + } + @Test @DisplayName("방과 멤버들에 해당하는 매칭결과가 없으면 예외를 발생한다.") void completeReview_throw_exception_when_not_exist_room_and_members() { From 1f2e3437c8e6da9dd092481214735b7e098b5bed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 05:50:33 +0900 Subject: [PATCH 04/31] =?UTF-8?q?[BE]=20=EC=B0=B8=EC=97=AC=ED=96=88?= =?UTF-8?q?=EB=8D=98=20=EB=B0=A9=EC=9D=B4=20=EC=A2=85=EB=A3=8C=EB=90=98?= =?UTF-8?q?=EB=A9=B4=20=EC=B0=B8=EC=97=AC=20=EC=A4=91=20=ED=83=AD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EA=B1=B0(#581)=20(#584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 참여했던 방이 종료되면 참여 중 탭에서 제거하는 기능 구현 * [BE] 중복으로 매칭되던 상황 해결(#587) (#588) * refactor: 도메인 수정 * feat: 리뷰어, 리뷰이 조회 API 기능 구현 * refactor: 중복된 기능 코드 제거 * docs: 메서드 시그니쳐 통일 * feat: 시연용 데이터 추가 * refactor: 패키지 이동으로 인한 오류 수정 * feat: 시연용 데이터 추가 * fix: REMOTE_ORIGIN 그냥 변수로 변경 * feat: 데이터 추가 * feat: 서브모듈 반영 * feat: response 생성 때 reviewer, reviewee 분리 * feat: application 설정 변경 * feat: 데모 데이터 함수로 분리 * fix: 누락된 saveAll 추가 * fix: 데이터 정합성 수정 * fix: roomId 상수 변경 * feat: 피드백 키워드 뒤 .제거 * refactor: 3차 데모데이 데이터 변경 * feat: room 4에 대한 케이스도 추가 * feat: room 4 매칭 추가 * fix: 응답 내 프로필 링크로 변경 * 최신 브랜치 병합 * feat: submodule 업데이트 * refactor: 서브모듈 변경 * fix: 매칭 중복 예외 처리 안되던 오류 해결 * fix: 리뷰어 매칭 안되던 오류 수정 * test: participationFilter 로직 수정에 따른 테스트 수정 --------- Co-authored-by: hjk0761 Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> * feat: pr 제출 안 했을 때 문구 추가 (#590) Co-authored-by: jinsil * [BE] 방 생성 검증 로직 주석 처리(#593) (#594) * refactor: 방 생성 검증 로직 주석 처리 * refactor: 운영 환경 설정 변경 * refactor: 방 생성 검증 로직 테스트 disabled 처리 --------- Co-authored-by: gyungchan Jo * [BE] 방 매칭 실패 시 매칭 실패 원인을 전달하는 기능 구현(#562) (#575) * feat: 반환된 예외를 통해 실패 원인을 찾는 기능 구현 * feat: 실패한 매칭 정보를 저장하는 엔티티 구현 * feat: 매칭을 실패했을 경우 실패한 매칭 정보를 저장하는 기능 구현 * refactor: 클래스명 변경 * feat: 반환된 예외를 통해 매칭 실패 원인에 대한 메세지를 얻는 기능 구현 * refactor: FailedMatching 생성자 변경 * feat: 매칭 실패한 방을 조회시, 실패 원인을 같이 전달하는 기능 구현 * refactor: 맵 구현체 변경 * refactor: 피드백 반영 --------- Co-authored-by: gyungchan Jo * [BE] 방 종료 시, 코드 리뷰 완료 버튼 동작 안하도록 하는 기능 구현(#579) (#580) * feat: 방 종료 후 코드 리뷰 완료 버튼 검증 로직 구현 * fix: 잘못된 요청값 수정 (#566) Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> * refactor: 리뷰 완료 요청 검증 로직 변경 --------- Co-authored-by: gyungchan Jo Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> --------- Co-authored-by: gyungchan Jo Co-authored-by: ashsty <77227961+ashsty@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: hjk0761 Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> Co-authored-by: jinsil --- .../participation/domain/Participation.java | 24 +++++++++--------- .../src/main/java/corea/room/domain/Room.java | 4 +++ .../java/corea/room/service/RoomService.java | 15 ++++++----- .../corea/room/service/RoomServiceTest.java | 25 ++++++++++++++++++- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/backend/src/main/java/corea/participation/domain/Participation.java b/backend/src/main/java/corea/participation/domain/Participation.java index 9a531c274..18f858207 100644 --- a/backend/src/main/java/corea/participation/domain/Participation.java +++ b/backend/src/main/java/corea/participation/domain/Participation.java @@ -66,18 +66,6 @@ public void participate() { room.participate(); } - public long getRoomsId() { - return room.getId(); - } - - public long getMembersId() { - return member.getId(); - } - - public String getMemberGithubId() { - return member.getGithubUserId(); - } - public boolean isReviewer() { return memberRole.isReviewer(); } @@ -90,6 +78,18 @@ public boolean isPullRequestNotSubmitted() { return status.isPullRequestNotSubmitted(); } + public long getRoomsId() { + return room.getId(); + } + + public long getMembersId() { + return member.getId(); + } + + public String getMemberGithubId() { + return member.getGithubUserId(); + } + private static void debug(long roomId, long memberId) { log.debug("참가자 생성[방 ID={}, 멤버 ID={}", roomId, memberId); } diff --git a/backend/src/main/java/corea/room/domain/Room.java b/backend/src/main/java/corea/room/domain/Room.java index e18773ab0..c64339ee8 100644 --- a/backend/src/main/java/corea/room/domain/Room.java +++ b/backend/src/main/java/corea/room/domain/Room.java @@ -111,6 +111,10 @@ public boolean isClosed() { return status.isClosed(); } + public boolean isNotClosed() { + return !isClosed(); + } + public boolean isNotProgress() { return status.isNotProgress(); } diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index e7f081d4c..5bb1b292d 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -96,15 +96,18 @@ private RoomResponse createRoomResponseWithParticipation(Room room, Participatio } public RoomResponses findParticipatedRooms(long memberId) { - List participations = participationRepository.findAllByMemberId(memberId); - List roomIds = participations.stream() - .map(Participation::getRoomsId) - .toList(); - - List rooms = roomRepository.findAllByIdInOrderByReviewDeadlineAsc(roomIds); + List rooms = findNonClosedParticipatedRooms(memberId); return RoomResponses.of(rooms, MemberRole.NONE, ParticipationStatus.PARTICIPATED, true, 0); } + private List findNonClosedParticipatedRooms(long memberId) { + return participationRepository.findAllByMemberId(memberId) + .stream() + .map(Participation::getRoom) + .filter(Room::isNotClosed) + .toList(); + } + public RoomResponses findRoomsWithRoomStatus(long memberId, int pageNumber, String expression, RoomStatus roomStatus) { RoomClassification classification = RoomClassification.from(expression); return getRoomResponses(memberId, pageNumber, classification, roomStatus); diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index ef26c0c86..dd61a061f 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -156,7 +156,7 @@ void findOne_with_matching_fail() { } @Test - @DisplayName("현재 로그인한 멤버가 참여 중인 방을 리뷰 마감일이 임박한 순으로 보여준다.") + @DisplayName("현재 로그인한 멤버가 참여 중인 방을 리뷰 마감일이 임박한 순으로 볼 수 있다.") void findParticipatedRooms() { Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); @@ -175,6 +175,29 @@ void findParticipatedRooms() { assertThat(managerNames).containsExactly("조경찬", "박민아"); } + @Test + @DisplayName("현재 로그인한 멤버가 참여 중인 방을 볼 때, 종료된 방은 포함되지 않는다.") + void findNonClosedParticipatedRooms() { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); + + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3))); + Room movinRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(movin)); + + Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + Long joysonId = joyson.getId(); + participationRepository.save(new Participation(pororoRoom, joyson, MemberRole.BOTH, pororoRoom.getMatchingSize())); + participationRepository.save(new Participation(ashRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + participationRepository.save(new Participation(movinRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + + RoomResponses response = roomService.findParticipatedRooms(joysonId); + List managerNames = getManagerNames(response); + + assertThat(managerNames).containsExactly("조경찬", "박민아"); + } + @ParameterizedTest @EnumSource(RoomStatus.class) @DisplayName("로그인한 사용자가 자신이 참여하지 않은 방을 상태별로 마감일 임박순으로 조회할 수 있다.") From f6beb1aae1438c4a8de5cfcb403cd4db82904f28 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:32:57 +0900 Subject: [PATCH 05/31] =?UTF-8?q?refactor:=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=EC=9A=A9=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=B3=80=EA=B2=BD=20(#600)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: gyungchan Jo --- backend/src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index fcbd9b2f8..f29c82937 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit fcbd9b2f8cec0d35f17a17156a26b1386293f668 +Subproject commit f29c8293751596007a7b57abcd385abd2e2e44cc From 83c7406eb9b57bc1cb60620a60a556fb60f53957 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:00:04 +0900 Subject: [PATCH 06/31] =?UTF-8?q?[BE]=20Room=20Controller=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EB=B6=84=EB=A6=AC(#595)=20(#596)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Room Controller 역할 분리 * refactor: 피드백 반영 --------- Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> --- .../controller/MatchingResultController.java | 32 +++++++ ...MatchingResultControllerSpecification.java | 35 +++++++ .../corea/room/controller/RoomController.java | 40 -------- .../RoomControllerSpecification.java | 72 --------------- .../controller/RoomInquiryController.java | 45 +++++++++ .../RoomInquiryControllerSpecification.java | 60 ++++++++++++ .../room/service/RoomInquiryService.java | 40 ++++++++ .../java/corea/room/service/RoomService.java | 17 ---- .../room/service/RoomInquiryServiceTest.java | 91 +++++++++++++++++++ .../corea/room/service/RoomServiceTest.java | 50 ---------- 10 files changed, 303 insertions(+), 179 deletions(-) create mode 100644 backend/src/main/java/corea/matchresult/controller/MatchingResultController.java create mode 100644 backend/src/main/java/corea/matchresult/controller/MatchingResultControllerSpecification.java create mode 100644 backend/src/main/java/corea/room/controller/RoomInquiryController.java create mode 100644 backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java create mode 100644 backend/src/main/java/corea/room/service/RoomInquiryService.java create mode 100644 backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java diff --git a/backend/src/main/java/corea/matchresult/controller/MatchingResultController.java b/backend/src/main/java/corea/matchresult/controller/MatchingResultController.java new file mode 100644 index 000000000..9bcb253cc --- /dev/null +++ b/backend/src/main/java/corea/matchresult/controller/MatchingResultController.java @@ -0,0 +1,32 @@ +package corea.matchresult.controller; + +import corea.auth.annotation.LoginMember; +import corea.auth.domain.AuthInfo; +import corea.matchresult.dto.MatchResultResponses; +import corea.matchresult.service.MatchResultService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/rooms") +@RequiredArgsConstructor +public class MatchingResultController implements MatchingResultControllerSpecification { + + private final MatchResultService matchResultService; + + @GetMapping("/{id}/reviewers") + public ResponseEntity reviewers(@PathVariable long id, @LoginMember AuthInfo authInfo) { + MatchResultResponses response = matchResultService.findReviewers(authInfo.getId(), id); + return ResponseEntity.ok(response); + } + + @GetMapping("/{id}/reviewees") + public ResponseEntity reviewees(@PathVariable long id, @LoginMember AuthInfo authInfo) { + MatchResultResponses response = matchResultService.findReviewees(authInfo.getId(), id); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/corea/matchresult/controller/MatchingResultControllerSpecification.java b/backend/src/main/java/corea/matchresult/controller/MatchingResultControllerSpecification.java new file mode 100644 index 000000000..ae97acb3b --- /dev/null +++ b/backend/src/main/java/corea/matchresult/controller/MatchingResultControllerSpecification.java @@ -0,0 +1,35 @@ +package corea.matchresult.controller; + +import corea.auth.domain.AuthInfo; +import corea.exception.ExceptionType; +import corea.global.annotation.ApiErrorResponses; +import corea.matchresult.dto.MatchResultResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.http.ResponseEntity; + +public interface MatchingResultControllerSpecification { + @Operation(summary = "해당 방에서 나에게 배정된 리뷰어들의 정보를 반환합니다.", + description = "해당 방에서 자신에게 배정된 리뷰어를 확인합니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + @ApiErrorResponses(value = {ExceptionType.MEMBER_NOT_FOUND, ExceptionType.ROOM_NOT_FOUND}) + ResponseEntity reviewers(@Parameter(description = "방 아이디", example = "1") + long id, + AuthInfo authInfo); + + @Operation(summary = "해당 방에서 나에게 배정된 리뷰이들의 정보를 반환합니다.", + description = "해당 방에서 자신에게 배정된 리뷰이를 확인합니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + @ApiErrorResponses(value = {ExceptionType.MEMBER_NOT_FOUND, ExceptionType.ROOM_NOT_FOUND}) + ResponseEntity reviewees(@Parameter(description = "방 아이디", example = "1") + long id, + AuthInfo authInfo); +} diff --git a/backend/src/main/java/corea/room/controller/RoomController.java b/backend/src/main/java/corea/room/controller/RoomController.java index 28aa50ad4..383801fdb 100644 --- a/backend/src/main/java/corea/room/controller/RoomController.java +++ b/backend/src/main/java/corea/room/controller/RoomController.java @@ -3,9 +3,6 @@ import corea.auth.annotation.AccessedMember; import corea.auth.annotation.LoginMember; import corea.auth.domain.AuthInfo; -import corea.matchresult.dto.MatchResultResponses; -import corea.matchresult.service.MatchResultService; -import corea.room.domain.RoomStatus; import corea.room.dto.RoomCreateRequest; import corea.room.dto.RoomParticipantResponses; import corea.room.dto.RoomResponse; @@ -25,7 +22,6 @@ public class RoomController implements RoomControllerSpecification { private final RoomService roomService; - private final MatchResultService matchResultService; private final AutomaticUpdateService automaticUpdateService; private final AutomaticMatchingService automaticMatchingService; @@ -52,48 +48,12 @@ public ResponseEntity participants(@PathVariable long return ResponseEntity.ok(response); } - @GetMapping("/{id}/reviewers") - public ResponseEntity reviewers(@PathVariable long id, @LoginMember AuthInfo authInfo) { - MatchResultResponses response = matchResultService.findReviewers(authInfo.getId(), id); - return ResponseEntity.ok(response); - } - - @GetMapping("/{id}/reviewees") - public ResponseEntity reviewees(@PathVariable long id, @LoginMember AuthInfo authInfo) { - MatchResultResponses response = matchResultService.findReviewees(authInfo.getId(), id); - return ResponseEntity.ok(response); - } - @GetMapping("/participated") public ResponseEntity participatedRooms(@LoginMember AuthInfo authInfo) { RoomResponses response = roomService.findParticipatedRooms(authInfo.getId()); return ResponseEntity.ok(response); } - @GetMapping("/opened") - public ResponseEntity openedRooms(@AccessedMember AuthInfo authInfo, - @RequestParam(defaultValue = "0") int page, - @RequestParam(value = "classification", defaultValue = "all") String expression) { - RoomResponses response = roomService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.OPEN); - return ResponseEntity.ok(response); - } - - @GetMapping("/progress") - public ResponseEntity progressRooms(@AccessedMember AuthInfo authInfo, - @RequestParam(defaultValue = "0") int page, - @RequestParam(value = "classification", defaultValue = "all") String expression) { - RoomResponses response = roomService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.PROGRESS); - return ResponseEntity.ok(response); - } - - @GetMapping("/closed") - public ResponseEntity closedRooms(@AccessedMember AuthInfo authInfo, - @RequestParam(defaultValue = "0") int page, - @RequestParam(value = "classification", defaultValue = "all") String expression) { - RoomResponses response = roomService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.CLOSE); - return ResponseEntity.ok(response); - } - @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable long id, @LoginMember AuthInfo authInfo) { roomService.delete(id, authInfo.getId()); diff --git a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java index b8b380b02..25697fc76 100644 --- a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java +++ b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java @@ -50,30 +50,6 @@ ResponseEntity participants(@Parameter(description = " long id, AuthInfo authInfo); - @Operation(summary = "해당 방에서 나에게 배정된 리뷰어들의 정보를 반환합니다.", - description = "해당 방에서 자신에게 배정된 리뷰어를 확인합니다.
" + - "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + - "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + - "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + - "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + - "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = {ExceptionType.MEMBER_NOT_FOUND, ExceptionType.ROOM_NOT_FOUND}) - ResponseEntity reviewers(@Parameter(description = "방 아이디", example = "1") - long id, - AuthInfo authInfo); - - @Operation(summary = "해당 방에서 나에게 배정된 리뷰이들의 정보를 반환합니다.", - description = "해당 방에서 자신에게 배정된 리뷰이를 확인합니다.
" + - "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + - "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + - "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + - "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + - "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = {ExceptionType.MEMBER_NOT_FOUND, ExceptionType.ROOM_NOT_FOUND}) - ResponseEntity reviewees(@Parameter(description = "방 아이디", example = "1") - long id, - AuthInfo authInfo); - @Operation(summary = "참여 중인 방 정보를 반환합니다..", description = "해당 멤버가 참여 중인 방들의 정보를 리뷰 마감일이 임박한 순으로 정렬해 반환합니다.
" + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + @@ -83,54 +59,6 @@ ResponseEntity reviewees(@Parameter(description = "방 아 "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") ResponseEntity participatedRooms(AuthInfo authInfo); - @Operation(summary = "현재 모집 중인 방 정보를 반환합니다.", - description = "현재 모집 중인 방들의 정보를 모집 마감일이 임박한 순으로 정렬해 반환합니다. 이미 참여 중인 방들의 정보는 제외됩니다.
" + - "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + - "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + - "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + - "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + - "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = ExceptionType.NOT_FOUND_ERROR) - ResponseEntity openedRooms(AuthInfo authInfo, - - @Parameter(description = "페이지 정보", example = "1") - int page, - - @Parameter(description = "방 분야", example = "AN") - String expression); - - @Operation(summary = "현재 모집 완료된 방 정보를 반환합니다.", - description = "현재 모집 완료된 방들의 정보를 반환합니다. 이미 참여 중인 방들의 정보는 제외됩니다.
" + - "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + - "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + - "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + - "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + - "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = ExceptionType.NOT_FOUND_ERROR) - ResponseEntity progressRooms(AuthInfo authInfo, - - @Parameter(description = "페이지 정보", example = "1") - int page, - - @Parameter(description = "방 분야", example = "AN") - String expression); - - @Operation(summary = "현재 종료된 방 정보를 반환합니다.", - description = "현재 모든 진행이 종료된 방들의 정보를 반환합니다. 이미 참여 중인 방들의 정보는 제외됩니다.
" + - "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + - "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + - "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + - "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + - "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = ExceptionType.NOT_FOUND_ERROR) - ResponseEntity closedRooms(AuthInfo authInfo, - - @Parameter(description = "페이지 정보", example = "2") - int page, - - @Parameter(description = "방 분야", example = "FE") - String expression); - @Operation(summary = "방을 삭제합니다.", description = "이미 생성되어 있는 방을 삭제합니다.
" + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + diff --git a/backend/src/main/java/corea/room/controller/RoomInquiryController.java b/backend/src/main/java/corea/room/controller/RoomInquiryController.java new file mode 100644 index 000000000..6cc3d1e73 --- /dev/null +++ b/backend/src/main/java/corea/room/controller/RoomInquiryController.java @@ -0,0 +1,45 @@ +package corea.room.controller; + +import corea.auth.annotation.AccessedMember; +import corea.auth.domain.AuthInfo; +import corea.room.domain.RoomStatus; +import corea.room.dto.RoomResponses; +import corea.room.service.RoomInquiryService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/rooms") +@RequiredArgsConstructor +public class RoomInquiryController implements RoomInquiryControllerSpecification { + + private final RoomInquiryService roomInquiryService; + + @GetMapping("/opened") + public ResponseEntity openedRooms(@AccessedMember AuthInfo authInfo, + @RequestParam(defaultValue = "0") int page, + @RequestParam(value = "classification", defaultValue = "all") String expression) { + RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.OPEN); + return ResponseEntity.ok(response); + } + + @GetMapping("/progress") + public ResponseEntity progressRooms(@AccessedMember AuthInfo authInfo, + @RequestParam(defaultValue = "0") int page, + @RequestParam(value = "classification", defaultValue = "all") String expression) { + RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.PROGRESS); + return ResponseEntity.ok(response); + } + + @GetMapping("/closed") + public ResponseEntity closedRooms(@AccessedMember AuthInfo authInfo, + @RequestParam(defaultValue = "0") int page, + @RequestParam(value = "classification", defaultValue = "all") String expression) { + RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.CLOSE); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java b/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java new file mode 100644 index 000000000..14cc38b5b --- /dev/null +++ b/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java @@ -0,0 +1,60 @@ +package corea.room.controller; + +import corea.auth.domain.AuthInfo; +import corea.exception.ExceptionType; +import corea.global.annotation.ApiErrorResponses; +import corea.room.dto.RoomResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.http.ResponseEntity; + +public interface RoomInquiryControllerSpecification { + + @Operation(summary = "현재 모집 중인 방 정보를 반환합니다.", + description = "현재 모집 중인 방들의 정보를 모집 마감일이 임박한 순으로 정렬해 반환합니다. 이미 참여 중인 방들의 정보는 제외됩니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + @ApiErrorResponses(value = ExceptionType.NOT_FOUND_ERROR) + ResponseEntity openedRooms(AuthInfo authInfo, + + @Parameter(description = "페이지 정보", example = "1") + int page, + + @Parameter(description = "방 분야", example = "AN") + String expression); + + @Operation(summary = "현재 모집 완료된 방 정보를 반환합니다.", + description = "현재 모집 완료된 방들의 정보를 반환합니다. 이미 참여 중인 방들의 정보는 제외됩니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + @ApiErrorResponses(value = ExceptionType.NOT_FOUND_ERROR) + ResponseEntity progressRooms(AuthInfo authInfo, + + @Parameter(description = "페이지 정보", example = "1") + int page, + + @Parameter(description = "방 분야", example = "AN") + String expression); + + @Operation(summary = "현재 종료된 방 정보를 반환합니다.", + description = "현재 모든 진행이 종료된 방들의 정보를 반환합니다. 이미 참여 중인 방들의 정보는 제외됩니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + @ApiErrorResponses(value = ExceptionType.NOT_FOUND_ERROR) + ResponseEntity closedRooms(AuthInfo authInfo, + + @Parameter(description = "페이지 정보", example = "2") + int page, + + @Parameter(description = "방 분야", example = "FE") + String expression); +} diff --git a/backend/src/main/java/corea/room/service/RoomInquiryService.java b/backend/src/main/java/corea/room/service/RoomInquiryService.java new file mode 100644 index 000000000..0c93d2413 --- /dev/null +++ b/backend/src/main/java/corea/room/service/RoomInquiryService.java @@ -0,0 +1,40 @@ +package corea.room.service; + +import corea.member.domain.MemberRole; +import corea.participation.domain.ParticipationStatus; +import corea.room.domain.Room; +import corea.room.domain.RoomClassification; +import corea.room.domain.RoomStatus; +import corea.room.dto.RoomResponses; +import corea.room.repository.RoomRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RoomInquiryService { + private static final int PAGE_DISPLAY_SIZE = 8; + private final RoomRepository roomRepository; + + public RoomResponses findRoomsWithRoomStatus(long memberId, int pageNumber, String expression, RoomStatus roomStatus) { + RoomClassification classification = RoomClassification.from(expression); + return getRoomResponses(memberId, pageNumber, classification, roomStatus); + } + + private RoomResponses getRoomResponses(long memberId, int pageNumber, RoomClassification classification, RoomStatus status) { + PageRequest pageRequest = PageRequest.of(pageNumber, PAGE_DISPLAY_SIZE); + + if (classification.isAll()) { + Page roomsWithPage = roomRepository.findAllByMemberAndStatus(memberId, status, pageRequest); + return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber); + } + Page roomsWithPage = roomRepository.findAllByMemberAndClassificationAndStatus(memberId, classification, status, pageRequest); + return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber); + } +} diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index 5bb1b292d..839718414 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -39,7 +39,6 @@ public class RoomService { private static final int PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE = 1; private static final int PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE = 1; - private static final int PAGE_DISPLAY_SIZE = 8; private static final int RANDOM_DISPLAY_PARTICIPANTS_SIZE = 6; private final RoomRepository roomRepository; @@ -108,22 +107,6 @@ private List findNonClosedParticipatedRooms(long memberId) { .toList(); } - public RoomResponses findRoomsWithRoomStatus(long memberId, int pageNumber, String expression, RoomStatus roomStatus) { - RoomClassification classification = RoomClassification.from(expression); - return getRoomResponses(memberId, pageNumber, classification, roomStatus); - } - - private RoomResponses getRoomResponses(long memberId, int pageNumber, RoomClassification classification, RoomStatus status) { - PageRequest pageRequest = PageRequest.of(pageNumber, PAGE_DISPLAY_SIZE); - - if (classification.isAll()) { - Page roomsWithPage = roomRepository.findAllByMemberAndStatus(memberId, status, pageRequest); - return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber); - } - Page roomsWithPage = roomRepository.findAllByMemberAndClassificationAndStatus(memberId, classification, status, pageRequest); - return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber); - } - @Transactional public void delete(long roomId, long memberId) { Room room = getRoom(roomId); diff --git a/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java b/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java new file mode 100644 index 000000000..52aeed75c --- /dev/null +++ b/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java @@ -0,0 +1,91 @@ +package corea.room.service; + +import config.ServiceTest; +import corea.auth.domain.AuthInfo; +import corea.fixture.MemberFixture; +import corea.fixture.RoomFixture; +import corea.member.domain.Member; +import corea.member.repository.MemberRepository; +import corea.room.domain.RoomStatus; +import corea.room.dto.RoomResponse; +import corea.room.dto.RoomResponses; +import corea.room.repository.RoomRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ServiceTest +public class RoomInquiryServiceTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RoomInquiryService roomInquiryService; + + @Autowired + private RoomRepository roomRepository; + + @ParameterizedTest + @EnumSource(RoomStatus.class) + @DisplayName("로그인한 사용자가 자신이 참여하지 않은 방을 상태별로 마감일 임박순으로 조회할 수 있다.") + void findRoomsWithRoomStatus_login_member(RoomStatus roomStatus) { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + + roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2), roomStatus)); + roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); + + RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(pororo.getId(), 0, "all", roomStatus); + List managerNames = getManagerNames(response); + + assertThat(managerNames).containsExactly("박민아"); + } + + @ParameterizedTest + @EnumSource(RoomStatus.class) + @DisplayName("비로그인 사용자가 방을 상태별로 마감일 임박순으로 조회할 수 있다.") + void findRoomsWithRoomStatus_non_login_member(RoomStatus roomStatus) { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + + roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2), roomStatus)); + roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); + + AuthInfo anonymous = AuthInfo.getAnonymous(); + + RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(anonymous.getId(), 0, "all", roomStatus); + List managerNames = getManagerNames(response); + + assertThat(managerNames).containsExactly("조경찬", "박민아"); + } + + @ParameterizedTest + @CsvSource(value = {"0, false", "1, true"}) + @DisplayName("방을 조회할 때 전달받은 페이지가 마지막 페이지인지 판별할 수 있다.") + void isLastPage(int pageNumber, boolean expected) { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + for (int i = 0; i < 9; i++) { + roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo)); + } + + AuthInfo anonymous = AuthInfo.getAnonymous(); + RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(anonymous.getId(), pageNumber, "all", RoomStatus.OPEN); + + assertThat(response.isLastPage()).isEqualTo(expected); + } + + private List getManagerNames(RoomResponses response) { + return response.rooms() + .stream() + .map(RoomResponse::manager) + .toList(); + } +} diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index dd61a061f..16b6bb5f7 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -198,55 +198,6 @@ void findNonClosedParticipatedRooms() { assertThat(managerNames).containsExactly("조경찬", "박민아"); } - @ParameterizedTest - @EnumSource(RoomStatus.class) - @DisplayName("로그인한 사용자가 자신이 참여하지 않은 방을 상태별로 마감일 임박순으로 조회할 수 있다.") - void findRoomsWithRoomStatus_login_member(RoomStatus roomStatus) { - Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); - - roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2), roomStatus)); - roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); - - RoomResponses response = roomService.findRoomsWithRoomStatus(pororo.getId(), 0, "all", roomStatus); - List managerNames = getManagerNames(response); - - assertThat(managerNames).containsExactly("박민아"); - } - - @ParameterizedTest - @EnumSource(RoomStatus.class) - @DisplayName("비로그인 사용자가 방을 상태별로 마감일 임박순으로 조회할 수 있다.") - void findRoomsWithRoomStatus_non_login_member(RoomStatus roomStatus) { - Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); - - roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2), roomStatus)); - roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); - - AuthInfo anonymous = AuthInfo.getAnonymous(); - - RoomResponses response = roomService.findRoomsWithRoomStatus(anonymous.getId(), 0, "all", roomStatus); - List managerNames = getManagerNames(response); - - assertThat(managerNames).containsExactly("조경찬", "박민아"); - } - - @ParameterizedTest - @CsvSource(value = {"0, false", "1, true"}) - @DisplayName("방을 조회할 때 전달받은 페이지가 마지막 페이지인지 판별할 수 있다.") - void isLastPage(int pageNumber, boolean expected) { - Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); - for (int i = 0; i < 9; i++) { - roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo)); - } - - AuthInfo anonymous = AuthInfo.getAnonymous(); - RoomResponses response = roomService.findRoomsWithRoomStatus(anonymous.getId(), pageNumber, "all", RoomStatus.OPEN); - - assertThat(response.isLastPage()).isEqualTo(expected); - } - @Test @DisplayName("방을 생성한 방장의 참여 상태는 MANAGER다.") void create_participationStatus_manager() { @@ -331,7 +282,6 @@ void findParticipants_withNoPullRequestParticipants() { matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); - RoomParticipantResponses participants = assertDoesNotThrow(() -> roomService.findParticipants(room.getId(), manager.getId())); assertAll( From 6cd74847d5ef02c86e78b32ce20c04b6073bc0e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:43:22 +0900 Subject: [PATCH 07/31] =?UTF-8?q?[BE]=20=EC=9E=90=EB=8F=99=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EB=B0=8F=20=EC=9E=90=EB=8F=99=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4(#586)=20(#604)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Lock을 통한 자동 매칭 동시성 제어 기능 구현 * feat: Lock을 통한 자동 업데이트 동시성 제어 기능 구현 * feat: 자동 매칭 Lock 범위 최소화를 위한 클래스 구현 * feat: 자동 예약 Lock 범위 최소화를 위한 클래스 구현 * test: 자동 매칭 테스트 중복 코드 추상 클래스로 이동 * test: 자동 예약 테스트 중복 코드 추상 클래스로 이동 * refactor: Transactional 어노테이션 제거 * refactor: 피드백 반영 --------- Co-authored-by: gyungchan Jo --- .../AutomaticMatchingRepository.java | 7 +- .../repository/AutomaticUpdateRepository.java | 7 +- .../service/AutomaticMatchingExecutor.java | 82 ++-------- .../service/AutomaticUpdateExecutor.java | 78 +--------- .../scheduler/service/MatchingExecutor.java | 66 ++++++++ .../scheduler/service/UpdateExecutor.java | 75 ++++++++++ .../domain/MatchingFailedReasonTest.java | 1 - .../AutomaticMatchingExecutorTest.java | 136 ++++++++--------- .../service/AutomaticUpdateExecutorTest.java | 69 +++++++-- .../service/MatchingExecutorTest.java | 141 ++++++++++++++++++ .../scheduler/service/UpdateExecutorTest.java | 60 ++++++++ 11 files changed, 491 insertions(+), 231 deletions(-) create mode 100644 backend/src/main/java/corea/scheduler/service/MatchingExecutor.java create mode 100644 backend/src/main/java/corea/scheduler/service/UpdateExecutor.java create mode 100644 backend/src/test/java/corea/scheduler/service/MatchingExecutorTest.java create mode 100644 backend/src/test/java/corea/scheduler/service/UpdateExecutorTest.java diff --git a/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java b/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java index 1e45e1c08..8ce419049 100644 --- a/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java +++ b/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java @@ -2,14 +2,19 @@ import corea.scheduler.domain.AutomaticMatching; import corea.scheduler.domain.ScheduleStatus; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import java.util.List; import java.util.Optional; public interface AutomaticMatchingRepository extends JpaRepository { - Optional findByRoomId(long roomId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT am FROM AutomaticMatching am WHERE am.roomId = :roomId AND am.status = :status") + Optional findByRoomIdAndStatusForUpdate(long roomId, ScheduleStatus status); List findAllByStatus(ScheduleStatus status); diff --git a/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java b/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java index 29e281a53..cdfda4ca6 100644 --- a/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java +++ b/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java @@ -2,14 +2,19 @@ import corea.scheduler.domain.AutomaticUpdate; import corea.scheduler.domain.ScheduleStatus; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import java.util.List; import java.util.Optional; public interface AutomaticUpdateRepository extends JpaRepository { - Optional findByRoomId(long roomId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT au FROM AutomaticUpdate au WHERE au.roomId = :roomId AND au.status = :status") + Optional findByRoomIdAndStatusForUpdate(long roomId, ScheduleStatus status); List findAllByStatus(ScheduleStatus status); diff --git a/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java b/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java index d90f2d49a..ad34d9b4f 100644 --- a/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java +++ b/backend/src/main/java/corea/scheduler/service/AutomaticMatchingExecutor.java @@ -1,88 +1,24 @@ package corea.scheduler.service; -import corea.exception.CoreaException; -import corea.exception.ExceptionType; -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; +import corea.scheduler.domain.ScheduleStatus; import corea.scheduler.repository.AutomaticMatchingRepository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.transaction.annotation.Transactional; -@Slf4j @Component @RequiredArgsConstructor public class AutomaticMatchingExecutor { - private final PlatformTransactionManager transactionManager; - private final MatchingService matchingService; - private final PullRequestProvider pullRequestProvider; - private final RoomRepository roomRepository; - private final FailedMatchingRepository failedMatchingRepository; + private final MatchingExecutor matchingExecutor; private final AutomaticMatchingRepository automaticMatchingRepository; - @Async + @Transactional public void execute(long roomId) { - //TODO: 트랜잭션 분리 - TransactionTemplate template = new TransactionTemplate(transactionManager); - - try { - template.execute(status -> { - startMatching(roomId); - return null; - }); - } catch (CoreaException e) { - log.warn("매칭 실행 중 에러 발생: {}", e.getMessage(), e); - recordMatchingFailure(roomId, e.getExceptionType()); - } - } - - private void startMatching(long roomId) { - Room room = getRoom(roomId); - - PullRequestInfo pullRequestInfo = pullRequestProvider.getUntilDeadline(room.getRepositoryLink(), room.getRecruitmentDeadline()); - matchingService.match(roomId, pullRequestInfo); - - AutomaticMatching automaticMatching = getAutomaticMatchingByRoomId(roomId); - automaticMatching.updateStatusToDone(); - } - - private void recordMatchingFailure(long roomId, ExceptionType exceptionType) { - //TODO: 위와 동일 - TransactionTemplate template = new TransactionTemplate(transactionManager); - template.execute(status -> { - 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)); - } - - private AutomaticMatching getAutomaticMatchingByRoomId(long roomId) { - return automaticMatchingRepository.findByRoomId(roomId) - .orElseThrow(() -> new CoreaException(ExceptionType.AUTOMATIC_MATCHING_NOT_FOUND)); + automaticMatchingRepository.findByRoomIdAndStatusForUpdate(roomId, ScheduleStatus.PENDING) + .ifPresent(automaticMatching -> { + matchingExecutor.match(roomId); + automaticMatching.updateStatusToDone(); + }); } } diff --git a/backend/src/main/java/corea/scheduler/service/AutomaticUpdateExecutor.java b/backend/src/main/java/corea/scheduler/service/AutomaticUpdateExecutor.java index 19568b2f6..512dc426a 100644 --- a/backend/src/main/java/corea/scheduler/service/AutomaticUpdateExecutor.java +++ b/backend/src/main/java/corea/scheduler/service/AutomaticUpdateExecutor.java @@ -1,88 +1,24 @@ package corea.scheduler.service; -import corea.exception.CoreaException; -import corea.exception.ExceptionType; -import corea.feedback.domain.DevelopFeedback; -import corea.feedback.domain.SocialFeedback; -import corea.feedback.repository.DevelopFeedbackRepository; -import corea.feedback.repository.SocialFeedbackRepository; -import corea.matchresult.domain.MatchResult; -import corea.matchresult.domain.ReviewStatus; -import corea.matchresult.repository.MatchResultRepository; -import corea.member.domain.Member; -import corea.member.domain.MemberRole; -import corea.room.domain.Room; -import corea.room.repository.RoomRepository; -import corea.scheduler.domain.AutomaticUpdate; +import corea.scheduler.domain.ScheduleStatus; import corea.scheduler.repository.AutomaticUpdateRepository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Slf4j @Component @RequiredArgsConstructor public class AutomaticUpdateExecutor { - private final RoomRepository roomRepository; - private final MatchResultRepository matchResultRepository; - private final SocialFeedbackRepository socialFeedbackRepository; - private final DevelopFeedbackRepository developFeedbackRepository; + private final UpdateExecutor updateExecutor; private final AutomaticUpdateRepository automaticUpdateRepository; - @Async @Transactional public void execute(long roomId) { - Room room = getRoom(roomId); - room.updateStatusToClose(); - - updateReviewCount(roomId); - updateFeedbackPoint(roomId); - - AutomaticUpdate automaticUpdate = getAutomaticUpdateByRoomId(roomId); - automaticUpdate.updateStatusToDone(); - } - - private void updateReviewCount(long roomId) { - matchResultRepository.findAllByRoomIdAndReviewStatus(roomId, ReviewStatus.COMPLETE) - .forEach(this::increaseMembersReviewCountIn); - } - - private void increaseMembersReviewCountIn(MatchResult matchResult) { - Member reviewer = matchResult.getReviewer(); - reviewer.increaseReviewCount(MemberRole.REVIEWER); - - Member reviewee = matchResult.getReviewee(); - reviewee.increaseReviewCount(MemberRole.REVIEWEE); - } - - private void updateFeedbackPoint(long roomId) { - socialFeedbackRepository.findAllByRoomId(roomId) - .forEach(this::updateSocialFeedbackPoint); - - developFeedbackRepository.findAllByRoomId(roomId) - .forEach(this::updateDevelopFeedbackPoint); - } - - private void updateSocialFeedbackPoint(SocialFeedback socialFeedback) { - Member receiver = socialFeedback.getReceiver(); - receiver.updateAverageRating(socialFeedback.getEvaluatePoint()); - } - - private void updateDevelopFeedbackPoint(DevelopFeedback developFeedback) { - Member receiver = developFeedback.getReceiver(); - receiver.updateAverageRating(developFeedback.getEvaluatePoint()); - } - - private Room getRoom(long roomId) { - return roomRepository.findById(roomId) - .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND)); - } - - private AutomaticUpdate getAutomaticUpdateByRoomId(long roomId) { - return automaticUpdateRepository.findByRoomId(roomId) - .orElseThrow(() -> new CoreaException(ExceptionType.AUTOMATIC_UPDATE_NOT_FOUND)); + automaticUpdateRepository.findByRoomIdAndStatusForUpdate(roomId, ScheduleStatus.PENDING) + .ifPresent(automaticUpdate -> { + updateExecutor.update(roomId); + automaticUpdate.updateStatusToDone(); + }); } } diff --git a/backend/src/main/java/corea/scheduler/service/MatchingExecutor.java b/backend/src/main/java/corea/scheduler/service/MatchingExecutor.java new file mode 100644 index 000000000..4828d4e34 --- /dev/null +++ b/backend/src/main/java/corea/scheduler/service/MatchingExecutor.java @@ -0,0 +1,66 @@ +package corea.scheduler.service; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.matching.domain.PullRequestInfo; +import corea.matching.service.MatchingService; +import corea.matching.service.PullRequestProvider; +import corea.room.domain.Room; +import corea.room.repository.RoomRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MatchingExecutor { + + private final PlatformTransactionManager transactionManager; + private final PullRequestProvider pullRequestProvider; + private final MatchingService matchingService; + private final RoomRepository roomRepository; + + @Async + public void match(long roomId) { + //TODO: 트랜잭션 분리 + TransactionTemplate template = new TransactionTemplate(transactionManager); + + try { + template.execute(status -> { + startMatching(roomId); + return null; + }); + } catch (CoreaException e) { + log.warn("매칭 실행 중 에러 발생: {}", e.getMessage(), e); + updateRoomStatusToFail(roomId); + } + } + + private void startMatching(long roomId) { + Room room = getRoom(roomId); + PullRequestInfo pullRequestInfo = pullRequestProvider.getUntilDeadline(room.getRepositoryLink(), room.getRecruitmentDeadline()); + + matchingService.match(roomId, pullRequestInfo); + } + + + private void updateRoomStatusToFail(long roomId) { + //TODO: 위와 동일 + TransactionTemplate template = new TransactionTemplate(transactionManager); + template.execute(status -> { + Room room = getRoom(roomId); + room.updateStatusToFail(); + return null; + }); + } + + private Room getRoom(long roomId) { + return roomRepository.findById(roomId) + .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND)); + } +} diff --git a/backend/src/main/java/corea/scheduler/service/UpdateExecutor.java b/backend/src/main/java/corea/scheduler/service/UpdateExecutor.java new file mode 100644 index 000000000..77c7ecbb6 --- /dev/null +++ b/backend/src/main/java/corea/scheduler/service/UpdateExecutor.java @@ -0,0 +1,75 @@ +package corea.scheduler.service; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.feedback.domain.DevelopFeedback; +import corea.feedback.domain.SocialFeedback; +import corea.feedback.repository.DevelopFeedbackRepository; +import corea.feedback.repository.SocialFeedbackRepository; +import corea.matchresult.domain.MatchResult; +import corea.matchresult.domain.ReviewStatus; +import corea.matchresult.repository.MatchResultRepository; +import corea.member.domain.Member; +import corea.member.domain.MemberRole; +import corea.room.domain.Room; +import corea.room.repository.RoomRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class UpdateExecutor { + + private final RoomRepository roomRepository; + private final MatchResultRepository matchResultRepository; + private final SocialFeedbackRepository socialFeedbackRepository; + private final DevelopFeedbackRepository developFeedbackRepository; + + @Async + @Transactional + public void update(long roomId) { + Room room = getRoom(roomId); + room.updateStatusToClose(); + + updateReviewCount(roomId); + updateFeedbackPoint(roomId); + } + + private void updateReviewCount(long roomId) { + matchResultRepository.findAllByRoomIdAndReviewStatus(roomId, ReviewStatus.COMPLETE) + .forEach(this::increaseMembersReviewCountIn); + } + + private void increaseMembersReviewCountIn(MatchResult matchResult) { + Member reviewer = matchResult.getReviewer(); + reviewer.increaseReviewCount(MemberRole.REVIEWER); + + Member reviewee = matchResult.getReviewee(); + reviewee.increaseReviewCount(MemberRole.REVIEWEE); + } + + private void updateFeedbackPoint(long roomId) { + socialFeedbackRepository.findAllByRoomId(roomId) + .forEach(this::updateSocialFeedbackPoint); + + developFeedbackRepository.findAllByRoomId(roomId) + .forEach(this::updateDevelopFeedbackPoint); + } + + private void updateSocialFeedbackPoint(SocialFeedback socialFeedback) { + Member receiver = socialFeedback.getReceiver(); + receiver.updateAverageRating(socialFeedback.getEvaluatePoint()); + } + + private void updateDevelopFeedbackPoint(DevelopFeedback developFeedback) { + Member receiver = developFeedback.getReceiver(); + receiver.updateAverageRating(developFeedback.getEvaluatePoint()); + } + + private Room getRoom(long roomId) { + return roomRepository.findById(roomId) + .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND)); + } +} diff --git a/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java b/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java index e323bf04c..e357b2e7a 100644 --- a/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java +++ b/backend/src/test/java/corea/matchresult/domain/MatchingFailedReasonTest.java @@ -7,7 +7,6 @@ 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 { diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java index bd84e9850..fa32817ca 100644 --- a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java +++ b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingExecutorTest.java @@ -8,34 +8,32 @@ import corea.matching.infrastructure.dto.GithubUserResponse; import corea.matching.infrastructure.dto.PullRequestResponse; import corea.matching.service.PullRequestProvider; -import corea.matchresult.domain.MatchResult; -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.repository.ParticipationRepository; import corea.room.domain.Room; -import corea.room.domain.RoomStatus; import corea.room.repository.RoomRepository; import corea.scheduler.domain.AutomaticMatching; import corea.scheduler.repository.AutomaticMatchingRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ServiceTest @Import(TestAsyncConfig.class) @@ -53,29 +51,29 @@ class AutomaticMatchingExecutorTest { @Autowired private MemberRepository memberRepository; - @Autowired - private MatchResultRepository matchResultRepository; - @Autowired private ParticipationRepository participationRepository; - @Autowired - private FailedMatchingRepository failedMatchingRepository; - @MockBean private PullRequestProvider pullRequestProvider; private Room room; private Room emptyParticipantRoom; + private Member pororo; + private Member ash; + private Member joysun; + private Member movin; + private Member ten; + private Member cho; @BeforeEach void setUp() { - Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); - Member joysun = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); - Member ten = memberRepository.save(MemberFixture.MEMBER_TENTEN()); - Member cho = memberRepository.save(MemberFixture.MEMBER_CHOCO()); + pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + joysun = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); + ten = memberRepository.save(MemberFixture.MEMBER_TENTEN()); + cho = memberRepository.save(MemberFixture.MEMBER_CHOCO()); room = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusSeconds(3))); emptyParticipantRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusSeconds(3))); @@ -87,59 +85,61 @@ void setUp() { participationRepository.save(new Participation(room, ten, MemberRole.BOTH, room.getMatchingSize())); participationRepository.save(new Participation(room, cho, MemberRole.BOTH, room.getMatchingSize())); - Mockito.when(pullRequestProvider.getUntilDeadline(any(), any())) - .thenReturn(new PullRequestInfo(Map.of( - pororo.getGithubUserId(), - new PullRequestResponse("link", new GithubUserResponse(pororo.getGithubUserId()), - LocalDateTime.of(2024, 10, 12, 18, 00)), - ash.getGithubUserId(), - new PullRequestResponse("link", new GithubUserResponse(ash.getGithubUserId()), - LocalDateTime.of(2024, 10, 12, 18, 20)), - joysun.getGithubUserId(), - new PullRequestResponse("link", new GithubUserResponse(joysun.getGithubUserId()), - LocalDateTime.of(2024, 10, 12, 18, 30)), - movin.getGithubUserId(), - new PullRequestResponse("link", new GithubUserResponse(movin.getGithubUserId()), - LocalDateTime.of(2024, 10, 12, 18, 10)), - ten.getGithubUserId(), - new PullRequestResponse("link", new GithubUserResponse(ten.getGithubUserId()), - LocalDateTime.of(2024, 10, 12, 18, 01)), - cho.getGithubUserId(), - new PullRequestResponse("link", new GithubUserResponse(cho.getGithubUserId()), - LocalDateTime.of(2024, 10, 12, 18, 01) - ) - ))); - } - - @Test - @DisplayName("매칭을 진행한다.") - void execute() { - AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(room.getId(), room.getRecruitmentDeadline())); - - automaticMatchingExecutor.execute(automaticMatching.getRoomId()); - - List matchResults = matchResultRepository.findAll(); - assertThat(matchResults).isNotEmpty(); + when(pullRequestProvider.getUntilDeadline(any(), any())) + .thenReturn(getPullRequestInfo(pororo, ash, joysun, movin, ten, cho)); } - @Transactional - @Test - @DisplayName("매칭 시도 중 예외가 발생했다면 방 상태를 FAIL로 변경한다.") - void updateRoomStatusToFail() { - AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(emptyParticipantRoom.getId(), emptyParticipantRoom.getRecruitmentDeadline())); - - automaticMatchingExecutor.execute(automaticMatching.getRoomId()); - - assertThat(emptyParticipantRoom.getStatus()).isEqualTo(RoomStatus.FAIL); + private PullRequestInfo getPullRequestInfo(Member pororo, Member ash, Member joysun, Member movin, Member ten, Member cho) { + return new PullRequestInfo(Map.of( + pororo.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(pororo.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 00)), + ash.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(ash.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 20)), + joysun.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(joysun.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 30)), + movin.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(movin.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 10)), + ten.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(ten.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 01)), + cho.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(cho.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 01) + ) + )); } @Test - @DisplayName("매칭 시도 중 예외가 발생했다면 매칭을 실패한 방을 저장한다.") - void saveFailedMatching() { - AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(emptyParticipantRoom.getId(), emptyParticipantRoom.getRecruitmentDeadline())); - - automaticMatchingExecutor.execute(automaticMatching.getRoomId()); - - assertThat(failedMatchingRepository.findByRoomId(emptyParticipantRoom.getId())).isNotEmpty(); + @DisplayName("동시에 10개의 자동 매칭을 실행해도 PESSIMISTIC_WRITE 락을 통해 동시성을 제어할 수 있다.") + void startMatchingWithLock() throws InterruptedException { + AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(room.getId(), LocalDateTime.now().plusDays(1))); + + int threadCount = 10; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicInteger successCount = new AtomicInteger(0); + + when(pullRequestProvider.getUntilDeadline(any(), any())).thenAnswer(ignore -> { + successCount.incrementAndGet(); + return getPullRequestInfo(pororo, ash, joysun, movin, ten, cho); + }); + + for (int i = 0; i < threadCount; i++) { + executorService.execute(() -> { + try { + automaticMatchingExecutor.execute(automaticMatching.getRoomId()); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + + assertThat(successCount.get()).isEqualTo(1); } } diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticUpdateExecutorTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticUpdateExecutorTest.java index 86653f25b..e28beafdb 100644 --- a/backend/src/test/java/corea/scheduler/service/AutomaticUpdateExecutorTest.java +++ b/backend/src/test/java/corea/scheduler/service/AutomaticUpdateExecutorTest.java @@ -4,21 +4,34 @@ import config.TestAsyncConfig; import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; +import corea.matchresult.domain.MatchResult; +import corea.matchresult.domain.ReviewStatus; +import corea.matchresult.repository.MatchResultRepository; import corea.member.domain.Member; import corea.member.repository.MemberRepository; import corea.room.domain.Room; -import corea.room.domain.RoomStatus; import corea.room.dto.RoomCreateRequest; import corea.room.repository.RoomRepository; import corea.scheduler.domain.AutomaticUpdate; import corea.scheduler.repository.AutomaticUpdateRepository; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; @ServiceTest @Import(TestAsyncConfig.class) @@ -27,31 +40,55 @@ class AutomaticUpdateExecutorTest { @Autowired private AutomaticUpdateExecutor automaticUpdateExecutor; + @Autowired + private AutomaticUpdateRepository automaticUpdateRepository; + @Autowired private RoomRepository roomRepository; @Autowired private MemberRepository memberRepository; - @Autowired - private AutomaticUpdateRepository automaticUpdateRepository; + @MockBean + private MatchResultRepository matchResultRepository; - @Transactional - @Test - @DisplayName("방 상태를 변경한다.") - void execute() { - Room room = getRoom(); - AutomaticUpdate automaticUpdate = automaticUpdateRepository.save(new AutomaticUpdate(room.getId(), room.getReviewDeadline())); + private Room room; - automaticUpdateExecutor.execute(automaticUpdate.getRoomId()); + @BeforeEach + void setUp() { + Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(); - assertThat(room.getStatus()).isEqualTo(RoomStatus.CLOSE); + room = roomRepository.save(request.toEntity(member)); } - private Room getRoom() { - Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(); + @Test + @DisplayName("동시에 10개의 자동 업데이트를 실행해도 PESSIMISTIC_WRITE 락을 통해 동시성을 제어할 수 있다.") + void startMatchingWithLock() throws InterruptedException { + AutomaticUpdate automaticUpdate = automaticUpdateRepository.save(new AutomaticUpdate(room.getId(), LocalDateTime.now().plusDays(1))); + + int threadCount = 10; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicInteger successCount = new AtomicInteger(0); + + when(matchResultRepository.findAllByRoomIdAndReviewStatus(anyLong(), any(ReviewStatus.class))).thenAnswer(ignore -> { + successCount.incrementAndGet(); + return Collections.singletonList(new MatchResult(room.getId(), MemberFixture.MEMBER_PORORO(), MemberFixture.MEMBER_MOVIN(), "")); + }); + + for (int i = 0; i < threadCount; i++) { + executorService.execute(() -> { + try { + automaticUpdateExecutor.execute(automaticUpdate.getRoomId()); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); - return roomRepository.save(request.toEntity(member)); + assertThat(successCount.get()).isEqualTo(1); } } diff --git a/backend/src/test/java/corea/scheduler/service/MatchingExecutorTest.java b/backend/src/test/java/corea/scheduler/service/MatchingExecutorTest.java new file mode 100644 index 000000000..0fd7d3480 --- /dev/null +++ b/backend/src/test/java/corea/scheduler/service/MatchingExecutorTest.java @@ -0,0 +1,141 @@ +package corea.scheduler.service; + +import config.ServiceTest; +import config.TestAsyncConfig; +import corea.fixture.MemberFixture; +import corea.fixture.RoomFixture; +import corea.matching.domain.PullRequestInfo; +import corea.matching.infrastructure.dto.GithubUserResponse; +import corea.matching.infrastructure.dto.PullRequestResponse; +import corea.matching.service.PullRequestProvider; +import corea.matchresult.domain.MatchResult; +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.repository.ParticipationRepository; +import corea.room.domain.Room; +import corea.room.domain.RoomStatus; +import corea.room.repository.RoomRepository; +import corea.scheduler.domain.AutomaticMatching; +import corea.scheduler.repository.AutomaticMatchingRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ServiceTest +@Import(TestAsyncConfig.class) +class MatchingExecutorTest { + + @Autowired + private MatchingExecutor matchingExecutor; + + @Autowired + private AutomaticMatchingRepository automaticMatchingRepository; + + @Autowired + private MatchResultRepository matchResultRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ParticipationRepository participationRepository; + + @MockBean + private PullRequestProvider pullRequestProvider; + + private Room room; + private Room emptyParticipantRoom; + private Member pororo; + private Member ash; + private Member joysun; + private Member movin; + private Member ten; + private Member cho; + + @BeforeEach + void setUp() { + pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + joysun = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); + ten = memberRepository.save(MemberFixture.MEMBER_TENTEN()); + cho = memberRepository.save(MemberFixture.MEMBER_CHOCO()); + + room = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusSeconds(3))); + emptyParticipantRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusSeconds(3))); + + participationRepository.save(new Participation(room, pororo, MemberRole.BOTH, room.getMatchingSize())); + participationRepository.save(new Participation(room, ash, MemberRole.BOTH, room.getMatchingSize())); + participationRepository.save(new Participation(room, joysun, MemberRole.BOTH, room.getMatchingSize())); + participationRepository.save(new Participation(room, movin, MemberRole.BOTH, room.getMatchingSize())); + participationRepository.save(new Participation(room, ten, MemberRole.BOTH, room.getMatchingSize())); + participationRepository.save(new Participation(room, cho, MemberRole.BOTH, room.getMatchingSize())); + + when(pullRequestProvider.getUntilDeadline(any(), any())) + .thenReturn(getPullRequestInfo(pororo, ash, joysun, movin, ten, cho)); + } + + private PullRequestInfo getPullRequestInfo(Member pororo, Member ash, Member joysun, Member movin, Member ten, Member cho) { + return new PullRequestInfo(Map.of( + pororo.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(pororo.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 00)), + ash.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(ash.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 20)), + joysun.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(joysun.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 30)), + movin.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(movin.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 10)), + ten.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(ten.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 01)), + cho.getGithubUserId(), + new PullRequestResponse("link", new GithubUserResponse(cho.getGithubUserId()), + LocalDateTime.of(2024, 10, 12, 18, 01) + ) + )); + } + + @Test + @DisplayName("매칭을 진행한다.") + void match() { + AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(room.getId(), room.getRecruitmentDeadline())); + + matchingExecutor.match(automaticMatching.getRoomId()); + + List matchResults = matchResultRepository.findAll(); + assertThat(matchResults).isNotEmpty(); + } + + @Transactional + @Test + @DisplayName("매칭 시도 중 예외가 발생했다면 방 상태를 FAIL로 변경한다.") + void matchFail() { + AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(emptyParticipantRoom.getId(), emptyParticipantRoom.getRecruitmentDeadline())); + + matchingExecutor.match(automaticMatching.getRoomId()); + + assertThat(emptyParticipantRoom.getStatus()).isEqualTo(RoomStatus.FAIL); + } +} diff --git a/backend/src/test/java/corea/scheduler/service/UpdateExecutorTest.java b/backend/src/test/java/corea/scheduler/service/UpdateExecutorTest.java new file mode 100644 index 000000000..a566975c7 --- /dev/null +++ b/backend/src/test/java/corea/scheduler/service/UpdateExecutorTest.java @@ -0,0 +1,60 @@ +package corea.scheduler.service; + +import config.ServiceTest; +import config.TestAsyncConfig; +import corea.fixture.MemberFixture; +import corea.fixture.RoomFixture; +import corea.member.domain.Member; +import corea.member.repository.MemberRepository; +import corea.room.domain.Room; +import corea.room.domain.RoomStatus; +import corea.room.dto.RoomCreateRequest; +import corea.room.repository.RoomRepository; +import corea.scheduler.domain.AutomaticUpdate; +import corea.scheduler.repository.AutomaticUpdateRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@ServiceTest +@Import(TestAsyncConfig.class) +class UpdateExecutorTest { + + @Autowired + private UpdateExecutor updateExecutor; + + @Autowired + private AutomaticUpdateRepository automaticUpdateRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private MemberRepository memberRepository; + + private Room room; + + @BeforeEach + void setUp() { + Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(); + + room = roomRepository.save(request.toEntity(member)); + } + + @Transactional + @Test + @DisplayName("방 상태를 변경한다.") + void execute() { + AutomaticUpdate automaticUpdate = automaticUpdateRepository.save(new AutomaticUpdate(room.getId(), room.getReviewDeadline())); + + updateExecutor.update(automaticUpdate.getRoomId()); + + assertThat(room.getStatus()).isEqualTo(RoomStatus.CLOSE); + } +} From ab2235bd6950383d5c7ca3d081364070e4d690be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:31:08 +0900 Subject: [PATCH 08/31] =?UTF-8?q?[BE]=20=EB=B0=A9=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20&=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81(#605)=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: repository 의존성 제거, 테스트 명확하게 변경 * feat: Reader/Writer 통해 조회 로직 분리 * feat: 룸 수정 기능 구현 * refactor: 피드백 반영 * fix: 충돌 해결 --------- Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> --- .../java/corea/exception/ExceptionType.java | 1 + .../corea/room/controller/RoomController.java | 23 +- .../RoomControllerSpecification.java | 15 +- .../corea/room/dto/RoomUpdateRequest.java | 76 +++++++ .../room/service/RoomAutomaticService.java | 84 +++++++ .../java/corea/room/service/RoomService.java | 35 +-- .../domain/AutomaticMatchingReader.java | 28 +++ .../domain/AutomaticMatchingWriter.java | 36 +++ .../domain/AutomaticUpdateReader.java | 28 +++ .../domain/AutomaticUpdateWriter.java | 39 ++++ .../AutomaticMatchingRepository.java | 2 + .../repository/AutomaticUpdateRepository.java | 2 + ...e.java => AutomaticMatchingScheduler.java} | 46 ++-- ...ice.java => AutomaticUpdateScheduler.java} | 46 ++-- .../test/java/corea/fixture/RoomFixture.java | 43 +++- .../corea/room/service/RoomServiceTest.java | 61 ++++-- .../AutomaticMatchingSchedulerTest.java | 168 ++++++++++++++ .../service/AutomaticMatchingServiceTest.java | 113 ---------- .../service/AutomaticUpdateSchedulerTest.java | 193 ++++++++++++++++ .../service/AutomaticUpdateServiceTest.java | 207 ------------------ 20 files changed, 832 insertions(+), 414 deletions(-) create mode 100644 backend/src/main/java/corea/room/dto/RoomUpdateRequest.java create mode 100644 backend/src/main/java/corea/room/service/RoomAutomaticService.java create mode 100644 backend/src/main/java/corea/scheduler/domain/AutomaticMatchingReader.java create mode 100644 backend/src/main/java/corea/scheduler/domain/AutomaticMatchingWriter.java create mode 100644 backend/src/main/java/corea/scheduler/domain/AutomaticUpdateReader.java create mode 100644 backend/src/main/java/corea/scheduler/domain/AutomaticUpdateWriter.java rename backend/src/main/java/corea/scheduler/service/{AutomaticMatchingService.java => AutomaticMatchingScheduler.java} (56%) rename backend/src/main/java/corea/scheduler/service/{AutomaticUpdateService.java => AutomaticUpdateScheduler.java} (58%) create mode 100644 backend/src/test/java/corea/scheduler/service/AutomaticMatchingSchedulerTest.java delete mode 100644 backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java create mode 100644 backend/src/test/java/corea/scheduler/service/AutomaticUpdateSchedulerTest.java delete mode 100644 backend/src/test/java/corea/scheduler/service/AutomaticUpdateServiceTest.java diff --git a/backend/src/main/java/corea/exception/ExceptionType.java b/backend/src/main/java/corea/exception/ExceptionType.java index 5e2a5c327..716e91079 100644 --- a/backend/src/main/java/corea/exception/ExceptionType.java +++ b/backend/src/main/java/corea/exception/ExceptionType.java @@ -9,6 +9,7 @@ public enum ExceptionType { ALREADY_APPLY(HttpStatus.BAD_REQUEST, "해당 방에 이미 참여했습니다."), NOT_ALREADY_APPLY(HttpStatus.BAD_REQUEST, "아직 참여하지 않은 방입니다."), ROOM_STATUS_INVALID(HttpStatus.BAD_REQUEST, "방이 마감되었습니다."), + MEMBER_IS_NOT_MANAGER(HttpStatus.BAD_REQUEST, "매니저가 아닙니다."), ROOM_PARTICIPANT_EXCEED(HttpStatus.BAD_REQUEST, "방 참여 인원 수가 최대입니다."), PARTICIPANT_SIZE_LACK(HttpStatus.BAD_REQUEST, "참여 인원이 부족하여 매칭을 진행할 수 없습니다."), PARTICIPANT_SIZE_LACK_DUE_TO_PULL_REQUEST(HttpStatus.BAD_REQUEST, "pull request 미제출로 인해 인원이 부족하여 매칭을 진행할 수 없습니다."), diff --git a/backend/src/main/java/corea/room/controller/RoomController.java b/backend/src/main/java/corea/room/controller/RoomController.java index 383801fdb..b9ae9cc30 100644 --- a/backend/src/main/java/corea/room/controller/RoomController.java +++ b/backend/src/main/java/corea/room/controller/RoomController.java @@ -3,13 +3,8 @@ import corea.auth.annotation.AccessedMember; import corea.auth.annotation.LoginMember; import corea.auth.domain.AuthInfo; -import corea.room.dto.RoomCreateRequest; -import corea.room.dto.RoomParticipantResponses; -import corea.room.dto.RoomResponse; -import corea.room.dto.RoomResponses; +import corea.room.dto.*; import corea.room.service.RoomService; -import corea.scheduler.service.AutomaticMatchingService; -import corea.scheduler.service.AutomaticUpdateService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -22,20 +17,23 @@ public class RoomController implements RoomControllerSpecification { private final RoomService roomService; - private final AutomaticUpdateService automaticUpdateService; - private final AutomaticMatchingService automaticMatchingService; @PostMapping public ResponseEntity create(@LoginMember AuthInfo authInfo, @RequestBody RoomCreateRequest request) { RoomResponse response = roomService.create(authInfo.getId(), request); - automaticMatchingService.matchOnRecruitmentDeadline(response); - automaticUpdateService.updateAtReviewDeadline(response); - return ResponseEntity.created(URI.create(String.format("/rooms/%d", response.id()))) .body(response); } + @PutMapping + public ResponseEntity update(@LoginMember AuthInfo authInfo, @RequestBody RoomUpdateRequest request) { + RoomResponse response = roomService.update(authInfo.getId(), request); + + return ResponseEntity.ok() + .body(response); + } + @GetMapping("/{id}") public ResponseEntity room(@PathVariable long id, @AccessedMember AuthInfo authInfo) { RoomResponse response = roomService.findOne(id, authInfo.getId()); @@ -58,9 +56,6 @@ public ResponseEntity participatedRooms(@LoginMember AuthInfo aut public ResponseEntity delete(@PathVariable long id, @LoginMember AuthInfo authInfo) { roomService.delete(id, authInfo.getId()); - automaticMatchingService.cancel(id); - automaticUpdateService.cancel(id); - return ResponseEntity.noContent() .build(); } diff --git a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java index 25697fc76..f29a4fc63 100644 --- a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java +++ b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java @@ -4,10 +4,7 @@ import corea.exception.ExceptionType; import corea.global.annotation.ApiErrorResponses; import corea.matchresult.dto.MatchResultResponses; -import corea.room.dto.RoomCreateRequest; -import corea.room.dto.RoomParticipantResponses; -import corea.room.dto.RoomResponse; -import corea.room.dto.RoomResponses; +import corea.room.dto.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,6 +23,16 @@ public interface RoomControllerSpecification { @ApiErrorResponses(value = ExceptionType.MEMBER_NOT_FOUND) ResponseEntity create(AuthInfo authInfo, RoomCreateRequest request); + @Operation(summary = "새로운 방을 수정합니다.", + description = "상호 리뷰 인원을 모을 수 있는 방을 수정합니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + @ApiErrorResponses(value = ExceptionType.MEMBER_NOT_FOUND) + ResponseEntity update(AuthInfo authInfo, RoomUpdateRequest request); + @Operation(summary = "방 상세 정보를 반환합니다.", description = "상세 페이지에 디스플레이 되는 방 상세 정보를 반환합니다.
" + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + diff --git a/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java b/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java new file mode 100644 index 000000000..404532f1c --- /dev/null +++ b/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java @@ -0,0 +1,76 @@ +package corea.room.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import corea.member.domain.Member; +import corea.room.domain.Room; +import corea.room.domain.RoomClassification; +import corea.room.domain.RoomStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "방 수정 요청") +public record RoomUpdateRequest( + @Schema(description = "방 ID", example = "99") + @NotBlank + long roomId, + + @Schema(description = "방 제목", example = "MVC를 아시나요?") + @NotBlank + String title, + + @Schema(description = "방 내용", example = "MVC 패턴을 아시나요?") + String content, + + @Schema(description = "repository 링크", example = "https://github.com/example/java-racingcar") + @NotBlank + String repositoryLink, + + @Schema(description = "썸네일 링크", example = "https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=13301655&filePath=L2Rpc2sxL25ld2RhdGEvMjAyMS8yMS9DTFMxMDAwNC8xMzMwMTY1NV9XUlRfMjFfQ0xTMTAwMDRfMjAyMTEyMTNfMQ==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10004") + String thumbnailLink, + + @Schema(description = "상호 리뷰 인원", example = "2") + @NotNull + int matchingSize, + + @Schema(description = "중심으로 리뷰하면 좋은 키워드", example = "[\"TDD\", \"클린코드\"]") + List keywords, + + @Schema(description = "제한 참여 인원", example = "200") + @NotNull + int limitedParticipants, + + @Schema(description = "모집 마감일", example = "2024-07-30 15:00") + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + LocalDateTime recruitmentDeadline, + + @Schema(description = "리뷰 마감일", example = "2024-08-10 23:59") + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + LocalDateTime reviewDeadline, + + @Schema(description = "방이 속하는 분야", example = "BE") + @NotNull + RoomClassification classification +) { + + private static final int INITIAL_PARTICIPANTS_SIZE = 1; + private static final RoomStatus INITIAL_ROOM_STATUS = RoomStatus.OPEN; + + public Room toEntity(Member manager) { + return new Room( + roomId, + title, content, + matchingSize, repositoryLink, + thumbnailLink, keywords, + INITIAL_PARTICIPANTS_SIZE, limitedParticipants, + manager, recruitmentDeadline, + reviewDeadline, classification, + INITIAL_ROOM_STATUS + ); + } +} diff --git a/backend/src/main/java/corea/room/service/RoomAutomaticService.java b/backend/src/main/java/corea/room/service/RoomAutomaticService.java new file mode 100644 index 000000000..008ae3674 --- /dev/null +++ b/backend/src/main/java/corea/room/service/RoomAutomaticService.java @@ -0,0 +1,84 @@ +package corea.room.service; + +import corea.room.domain.Room; +import corea.scheduler.domain.*; +import corea.scheduler.service.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RoomAutomaticService { + + private final AutomaticUpdateWriter automaticUpdateWriter; + private final AutomaticUpdateReader automaticUpdateReader; + + private final AutomaticMatchingWriter automaticMatchingWriter; + private final AutomaticMatchingReader automaticMatchingReader; + + private final AutomaticMatchingScheduler automaticMatchingScheduler; + private final AutomaticUpdateScheduler automaticUpdateScheduler; + + @Transactional + public void updateTime(Room updateRoom) { + AutomaticMatching automaticMatching = automaticMatchingReader.findWithRoom(updateRoom); + AutomaticUpdate automaticUpdate = automaticUpdateReader.findWithRoom(updateRoom); + + automaticMatchingWriter.updateTime(automaticMatching, updateRoom.getRecruitmentDeadline()); + automaticUpdateWriter.updateTime(automaticUpdate, updateRoom.getReviewDeadline()); + + automaticMatchingScheduler.modifyTask(updateRoom); + automaticUpdateScheduler.modifyTask(updateRoom); + } + + @Transactional + public void createAutomatic(Room room) { + automaticMatchingWriter.create(room); + automaticUpdateWriter.create(room); + + automaticMatchingScheduler.matchOnRecruitmentDeadline(room); + automaticUpdateScheduler.updateAtReviewDeadline(room); + } + + @Transactional + public void deleteAutomatic(Room room) { + AutomaticMatching automaticMatching = automaticMatchingReader.findWithRoom(room); + AutomaticUpdate automaticUpdate = automaticUpdateReader.findWithRoom(room); + + automaticMatchingWriter.delete(automaticMatching); + automaticUpdateWriter.delete(automaticUpdate); + + automaticMatchingScheduler.cancel(room.getId()); + automaticUpdateScheduler.cancel(room.getId()); + } + + @EventListener(ApplicationReadyEvent.class) + public void schedulePendingAutomaticMatching() { + List matchings = automaticMatchingReader.findAllByStatus(ScheduleStatus.PENDING); + + log.info("{}개의 방에 대해 자동 매칭 재예약 시작", matchings.size()); + + matchings.forEach(automaticMatchingScheduler::matchOnRecruitmentDeadline); + + log.info("{}개의 방에 대해 자동 매칭 재예약 완료", matchings.size()); + } + + @EventListener(ApplicationReadyEvent.class) + public void schedulePendingAutomaticUpdate() { + List updates = automaticUpdateReader.findAllByStatus(ScheduleStatus.PENDING); + + log.info("{}개의 방에 대해 자동 상태 업데이트 재예약 시작", updates.size()); + + updates.forEach(automaticUpdateScheduler::updateAtReviewDeadline); + + log.info("{}개의 방에 대해 자동 상태 업데이트 재예약 완료", updates.size()); + } +} diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index 839718414..f0a856032 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -11,18 +11,10 @@ import corea.participation.domain.ParticipationStatus; import corea.participation.repository.ParticipationRepository; import corea.room.domain.Room; -import corea.room.domain.RoomClassification; -import corea.room.domain.RoomStatus; import corea.room.dto.*; import corea.room.repository.RoomRepository; -import corea.scheduler.domain.AutomaticMatching; -import corea.scheduler.domain.AutomaticUpdate; -import corea.scheduler.repository.AutomaticMatchingRepository; -import corea.scheduler.repository.AutomaticUpdateRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,8 +38,8 @@ public class RoomService { private final MatchResultRepository matchResultRepository; private final ParticipationRepository participationRepository; private final FailedMatchingRepository failedMatchingRepository; - private final AutomaticMatchingRepository automaticMatchingRepository; - private final AutomaticUpdateRepository automaticUpdateRepository; + private final RoomAutomaticService roomAutomaticService; + @Transactional public RoomResponse create(long memberId, RoomCreateRequest request) { @@ -59,12 +51,28 @@ public RoomResponse create(long memberId, RoomCreateRequest request) { Participation participation = new Participation(room, manager); participationRepository.save(participation); - automaticMatchingRepository.save(new AutomaticMatching(room.getId(), request.recruitmentDeadline())); - automaticUpdateRepository.save(new AutomaticUpdate(room.getId(), request.reviewDeadline())); + roomAutomaticService.createAutomatic(room); return RoomResponse.of(room, participation.getMemberRole(), ParticipationStatus.MANAGER); } + @Transactional + public RoomResponse update(long memberId, RoomUpdateRequest request) { + Room room = getRoom(request.roomId()); + if (room.isNotMatchingManager(memberId)) { + throw new CoreaException(ExceptionType.MEMBER_IS_NOT_MANAGER); + } + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); + + Room updatedRoom = roomRepository.save(request.toEntity(member)); + Participation participation = participationRepository.findByRoomIdAndMemberId(updatedRoom.getId(), memberId) + .orElseThrow(() -> new CoreaException(ExceptionType.NOT_ALREADY_APPLY)); + + roomAutomaticService.updateTime(updatedRoom); + return RoomResponse.of(updatedRoom, participation.getMemberRole(), ParticipationStatus.MANAGER); + } + private void validateDeadLine(LocalDateTime recruitmentDeadline, LocalDateTime reviewDeadline) { LocalDateTime currentDateTime = LocalDateTime.now(); @@ -114,8 +122,7 @@ public void delete(long roomId, long memberId) { roomRepository.delete(room); participationRepository.deleteAllByRoomId(roomId); - automaticMatchingRepository.deleteByRoomId(roomId); - automaticUpdateRepository.deleteByRoomId(roomId); + roomAutomaticService.deleteAutomatic(room); } private void validateDeletionAuthority(Room room, long memberId) { diff --git a/backend/src/main/java/corea/scheduler/domain/AutomaticMatchingReader.java b/backend/src/main/java/corea/scheduler/domain/AutomaticMatchingReader.java new file mode 100644 index 000000000..4a45666c8 --- /dev/null +++ b/backend/src/main/java/corea/scheduler/domain/AutomaticMatchingReader.java @@ -0,0 +1,28 @@ +package corea.scheduler.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.room.domain.Room; +import corea.scheduler.repository.AutomaticMatchingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AutomaticMatchingReader { + + private final AutomaticMatchingRepository automaticMatchingRepository; + + public AutomaticMatching findWithRoom(Room room) { + return automaticMatchingRepository.findByRoomId(room.getId()) + .orElseThrow(() -> new CoreaException(ExceptionType.AUTOMATIC_MATCHING_NOT_FOUND)); + } + + public List findAllByStatus(ScheduleStatus status) { + return automaticMatchingRepository.findAllByStatus(status); + } +} diff --git a/backend/src/main/java/corea/scheduler/domain/AutomaticMatchingWriter.java b/backend/src/main/java/corea/scheduler/domain/AutomaticMatchingWriter.java new file mode 100644 index 000000000..0a7b82808 --- /dev/null +++ b/backend/src/main/java/corea/scheduler/domain/AutomaticMatchingWriter.java @@ -0,0 +1,36 @@ +package corea.scheduler.domain; + +import corea.room.domain.Room; +import corea.scheduler.repository.AutomaticMatchingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +@Transactional +public class AutomaticMatchingWriter { + + private final AutomaticMatchingRepository automaticMatchingRepository; + + public AutomaticMatching updateTime(AutomaticMatching automaticMatching, LocalDateTime matchingStartTime) { + AutomaticMatching updateEntity = new AutomaticMatching( + automaticMatching.getId(), + automaticMatching.getRoomId(), + matchingStartTime, + automaticMatching.getStatus() + ); + return automaticMatchingRepository.save(updateEntity); + } + + public AutomaticMatching create(Room room) { + AutomaticMatching entity = new AutomaticMatching(room.getId(),room.getRecruitmentDeadline()); + return automaticMatchingRepository.save(entity); + } + + public void delete(AutomaticMatching automaticMatching) { + automaticMatchingRepository.delete(automaticMatching); + } +} diff --git a/backend/src/main/java/corea/scheduler/domain/AutomaticUpdateReader.java b/backend/src/main/java/corea/scheduler/domain/AutomaticUpdateReader.java new file mode 100644 index 000000000..b6caff499 --- /dev/null +++ b/backend/src/main/java/corea/scheduler/domain/AutomaticUpdateReader.java @@ -0,0 +1,28 @@ +package corea.scheduler.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.room.domain.Room; +import corea.scheduler.repository.AutomaticUpdateRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AutomaticUpdateReader { + + private final AutomaticUpdateRepository automaticUpdateRepository; + + public AutomaticUpdate findWithRoom(Room room) { + return automaticUpdateRepository.findByRoomId(room.getId()) + .orElseThrow(() -> new CoreaException(ExceptionType.AUTOMATIC_UPDATE_NOT_FOUND)); + } + + public List findAllByStatus(ScheduleStatus status) { + return automaticUpdateRepository.findAllByStatus(status); + } +} diff --git a/backend/src/main/java/corea/scheduler/domain/AutomaticUpdateWriter.java b/backend/src/main/java/corea/scheduler/domain/AutomaticUpdateWriter.java new file mode 100644 index 000000000..fd1563073 --- /dev/null +++ b/backend/src/main/java/corea/scheduler/domain/AutomaticUpdateWriter.java @@ -0,0 +1,39 @@ +package corea.scheduler.domain; + +import corea.room.domain.Room; +import corea.scheduler.repository.AutomaticUpdateRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +@Transactional +public class AutomaticUpdateWriter { + + private final AutomaticUpdateRepository automaticUpdateRepository; + + public AutomaticUpdate updateTime(AutomaticUpdate automaticUpdate, LocalDateTime reviewDeadline) { + AutomaticUpdate updateEntity = new AutomaticUpdate( + automaticUpdate.getId(), + automaticUpdate.getRoomId(), + reviewDeadline, + automaticUpdate.getStatus() + ); + return automaticUpdateRepository.save(updateEntity); + } + + public AutomaticUpdate create(Room room) { + AutomaticUpdate createEntity = new AutomaticUpdate( + room.getId(), + room.getReviewDeadline() + ); + return automaticUpdateRepository.save(createEntity); + } + + public void delete(AutomaticUpdate automaticUpdate) { + automaticUpdateRepository.delete(automaticUpdate); + } +} diff --git a/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java b/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java index 8ce419049..b6222a210 100644 --- a/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java +++ b/backend/src/main/java/corea/scheduler/repository/AutomaticMatchingRepository.java @@ -19,4 +19,6 @@ public interface AutomaticMatchingRepository extends JpaRepository findAllByStatus(ScheduleStatus status); void deleteByRoomId(long roomId); + + Optional findByRoomId(long roomId); } diff --git a/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java b/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java index cdfda4ca6..d0a674270 100644 --- a/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java +++ b/backend/src/main/java/corea/scheduler/repository/AutomaticUpdateRepository.java @@ -18,5 +18,7 @@ public interface AutomaticUpdateRepository extends JpaRepository findAllByStatus(ScheduleStatus status); + Optional findByRoomId(long roomId); + void deleteByRoomId(long roomId); } diff --git a/backend/src/main/java/corea/scheduler/service/AutomaticMatchingService.java b/backend/src/main/java/corea/scheduler/service/AutomaticMatchingScheduler.java similarity index 56% rename from backend/src/main/java/corea/scheduler/service/AutomaticMatchingService.java rename to backend/src/main/java/corea/scheduler/service/AutomaticMatchingScheduler.java index 99f166907..b3f453ce1 100644 --- a/backend/src/main/java/corea/scheduler/service/AutomaticMatchingService.java +++ b/backend/src/main/java/corea/scheduler/service/AutomaticMatchingScheduler.java @@ -1,13 +1,9 @@ package corea.scheduler.service; -import corea.room.dto.RoomResponse; +import corea.room.domain.Room; import corea.scheduler.domain.AutomaticMatching; -import corea.scheduler.domain.ScheduleStatus; -import corea.scheduler.repository.AutomaticMatchingRepository; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,38 +11,45 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; @Slf4j @Service -@RequiredArgsConstructor @Transactional(readOnly = true) -public class AutomaticMatchingService { +public class AutomaticMatchingScheduler { private static final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul"); private final TaskScheduler taskScheduler; private final AutomaticMatchingExecutor automaticMatchingExecutor; - private final AutomaticMatchingRepository automaticMatchingRepository; + private final Map> scheduledTasks; - private final Map> scheduledTasks = new ConcurrentHashMap<>(); - - @EventListener(ApplicationReadyEvent.class) - public void schedulePendingAutomaticMatching() { - List matchings = automaticMatchingRepository.findAllByStatus(ScheduleStatus.PENDING); + @Autowired + public AutomaticMatchingScheduler(TaskScheduler taskScheduler, AutomaticMatchingExecutor automaticMatchingExecutor) { + this.taskScheduler = taskScheduler; + this.automaticMatchingExecutor = automaticMatchingExecutor; + this.scheduledTasks = new ConcurrentHashMap<>(); + } - log.info("{}개의 방에 대해 자동 매칭 재예약 시작", matchings.size()); + public AutomaticMatchingScheduler(TaskScheduler taskScheduler, AutomaticMatchingExecutor automaticMatchingExecutor, Map> scheduledTasks) { + this.taskScheduler = taskScheduler; + this.automaticMatchingExecutor = automaticMatchingExecutor; + this.scheduledTasks = scheduledTasks; + } - matchings.forEach(matching -> scheduleMatching(matching.getRoomId(), matching.getMatchingStartTime())); + public void modifyTask(Room room) { + cancel(room.getId()); + scheduleMatching(room.getId(), room.getRecruitmentDeadline()); + } - log.info("{}개의 방에 대해 자동 매칭 재예약 완료", matchings.size()); + public void matchOnRecruitmentDeadline(Room room) { + scheduleMatching(room.getId(), room.getRecruitmentDeadline()); } - public void matchOnRecruitmentDeadline(RoomResponse response) { - scheduleMatching(response.id(), response.recruitmentDeadline()); + public void matchOnRecruitmentDeadline(AutomaticMatching automaticMatching) { + scheduleMatching(automaticMatching.getRoomId(), automaticMatching.getMatchingStartTime()); } private void scheduleMatching(long roomId, LocalDateTime matchingStartTime) { @@ -60,7 +63,8 @@ private void scheduleMatching(long roomId, LocalDateTime matchingStartTime) { } private Instant toInstant(LocalDateTime matchingStartTime) { - return matchingStartTime.atZone(ZONE_ID).toInstant(); + return matchingStartTime.atZone(ZONE_ID) + .toInstant(); } public void cancel(long roomId) { diff --git a/backend/src/main/java/corea/scheduler/service/AutomaticUpdateService.java b/backend/src/main/java/corea/scheduler/service/AutomaticUpdateScheduler.java similarity index 58% rename from backend/src/main/java/corea/scheduler/service/AutomaticUpdateService.java rename to backend/src/main/java/corea/scheduler/service/AutomaticUpdateScheduler.java index 082a812ea..e03c5c0b6 100644 --- a/backend/src/main/java/corea/scheduler/service/AutomaticUpdateService.java +++ b/backend/src/main/java/corea/scheduler/service/AutomaticUpdateScheduler.java @@ -1,13 +1,9 @@ package corea.scheduler.service; -import corea.room.dto.RoomResponse; +import corea.room.domain.Room; import corea.scheduler.domain.AutomaticUpdate; -import corea.scheduler.domain.ScheduleStatus; -import corea.scheduler.repository.AutomaticUpdateRepository; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,38 +11,43 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; @Slf4j @Service -@RequiredArgsConstructor @Transactional(readOnly = true) -public class AutomaticUpdateService { +public class AutomaticUpdateScheduler { private static final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul"); private final TaskScheduler taskScheduler; private final AutomaticUpdateExecutor automaticUpdateExecutor; - private final AutomaticUpdateRepository automaticUpdateRepository; + private final Map> scheduledTasks; - private final Map> scheduledTasks = new ConcurrentHashMap<>(); - - @EventListener(ApplicationReadyEvent.class) - public void schedulePendingAutomaticUpdate() { - List updates = automaticUpdateRepository.findAllByStatus(ScheduleStatus.PENDING); + @Autowired + public AutomaticUpdateScheduler(TaskScheduler taskScheduler, AutomaticUpdateExecutor automaticUpdateExecutor) { + this(taskScheduler,automaticUpdateExecutor,new ConcurrentHashMap<>()); + } - log.info("{}개의 방에 대해 자동 상태 업데이트 재예약 시작", updates.size()); + public AutomaticUpdateScheduler(TaskScheduler taskScheduler, AutomaticUpdateExecutor automaticUpdateExecutor, Map> scheduledTasks) { + this.taskScheduler = taskScheduler; + this.automaticUpdateExecutor = automaticUpdateExecutor; + this.scheduledTasks = scheduledTasks; + } - updates.forEach(update -> scheduleUpdate(update.getRoomId(), update.getUpdateStartTime())); + public void updateAtReviewDeadline(Room room) { + scheduleUpdate(room.getId(), room.getReviewDeadline()); + } - log.info("{}개의 방에 대해 자동 상태 업데이트 재예약 완료", updates.size()); + public void updateAtReviewDeadline(AutomaticUpdate automaticUpdate) { + scheduleUpdate(automaticUpdate.getRoomId(), automaticUpdate.getUpdateStartTime()); } - public void updateAtReviewDeadline(RoomResponse response) { - scheduleUpdate(response.id(), response.reviewDeadline()); + public void modifyTask(Room room) { + cancel(room.getId()); + scheduleUpdate(room.getId(), room.getReviewDeadline()); } private void scheduleUpdate(long roomId, LocalDateTime updateStartTime) { @@ -54,13 +55,13 @@ private void scheduleUpdate(long roomId, LocalDateTime updateStartTime) { () -> automaticUpdateExecutor.execute(roomId), toInstant(updateStartTime) ); - log.info("{}번 방 자동 상태 업데이트 예약 - 예약 시간: {}", roomId, updateStartTime); scheduledTasks.put(roomId, schedule); } private Instant toInstant(LocalDateTime updateStartTime) { - return updateStartTime.atZone(ZONE_ID).toInstant(); + return updateStartTime.atZone(ZONE_ID) + .toInstant(); } public void cancel(long roomId) { @@ -74,7 +75,6 @@ public void cancel(long roomId) { private void cancelScheduledUpdate(long roomId) { ScheduledFuture scheduledUpdate = scheduledTasks.remove(roomId); scheduledUpdate.cancel(true); - log.info("{}번 방 기존 자동 상태 업데이트 예약 취소", roomId); } } diff --git a/backend/src/test/java/corea/fixture/RoomFixture.java b/backend/src/test/java/corea/fixture/RoomFixture.java index abe12b33e..7192ba214 100644 --- a/backend/src/test/java/corea/fixture/RoomFixture.java +++ b/backend/src/test/java/corea/fixture/RoomFixture.java @@ -5,10 +5,12 @@ import corea.room.domain.RoomClassification; import corea.room.domain.RoomStatus; import corea.room.dto.RoomCreateRequest; +import corea.room.dto.RoomUpdateRequest; import java.time.LocalDateTime; import java.util.List; +//@formatter:off public class RoomFixture { public static Room ROOM_DOMAIN(Member member) { @@ -19,6 +21,10 @@ public static Room ROOM_DOMAIN(Member member, LocalDateTime recruitmentDeadline) return ROOM_DOMAIN(member, recruitmentDeadline, RoomStatus.OPEN); } + public static Room ROOM_DOMAIN_REVIEW_DEADLINE(Member member,LocalDateTime reviewDeadline) { + return ROOM_DOMAIN(member, LocalDateTime.now().plusDays(2),reviewDeadline, RoomStatus.OPEN); + } + public static Room ROOM_DOMAIN_WITH_CLOSED(Member member) { return ROOM_DOMAIN(member, LocalDateTime.now(), RoomStatus.CLOSE); } @@ -27,6 +33,24 @@ public static Room ROOM_DOMAIN_WITH_PROGRESS(Member member) { return ROOM_DOMAIN(member, LocalDateTime.now(), RoomStatus.PROGRESS); } + public static Room ROOM_DOMAIN(Member member, LocalDateTime recruitmentDeadline, LocalDateTime reviewDeadline, RoomStatus status) { + return new Room( + "자바 레이싱 카 - MVC", + "MVC 패턴을 아시나요?", + 2, + "https://github.com/example/java-racingcar", + "https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=13301655&filePath=L2Rpc2sxL25ld2RhdGEvMjAyMS8yMS9DTFMxMDAwNC8xMzMwMTY1NV9XUlRfMjFfQ0xTMTAwMDRfMjAyMTEyMTNfMQ==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10004", + List.of("TDD, 클린코드,자바"), + 17, + 30, + member, + recruitmentDeadline, + reviewDeadline, + RoomClassification.BACKEND, + status + ); + } + public static Room ROOM_DOMAIN(Member member, LocalDateTime recruitmentDeadline, RoomStatus status) { return new Room( "자바 레이싱 카 - MVC", @@ -63,6 +87,21 @@ public static Room ROOM_DOMAIN(Long id, Member member) { RoomStatus.OPEN ); } + public static RoomUpdateRequest ROOM_UPDATE_REQUEST(long roomId){ + return new RoomUpdateRequest( + roomId, + "Test Room", + "Test Content", + "https://github.com/youngsu5582/github-api-test", + "https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=13301655&filePath=L2Rpc2sxL25ld2RhdGEvMjAyMS8yMS9DTFMxMDAwNC8xMzMwMTY1NV9XUlRfMjFfQ0xTMTAwMDRfMjAyMTEyMTNfMQ==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10004", + 2, + List.of("TDD, 클린코드, 자바"), + 10, + LocalDateTime.now(), + LocalDateTime.now().plusDays(14), + RoomClassification.BACKEND + ); + } public static RoomCreateRequest ROOM_CREATE_REQUEST() { return ROOM_CREATE_REQUEST(LocalDateTime.now().plusHours(2), LocalDateTime.now().plusDays(2)); @@ -104,7 +143,7 @@ public static Room ROOM_PULL_REQUEST(Member member) { member, LocalDateTime.now().plusSeconds(100), LocalDateTime.now().plusDays(1), - RoomClassification.BACKEND, - RoomStatus.OPEN); + RoomClassification.BACKEND, RoomStatus.OPEN); } } +//@formatter:on diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index 16b6bb5f7..761f3c74d 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -1,7 +1,6 @@ package corea.room.service; import config.ServiceTest; -import corea.auth.domain.AuthInfo; import corea.exception.CoreaException; import corea.exception.ExceptionType; import corea.fixture.MatchResultFixture; @@ -17,7 +16,6 @@ import corea.participation.domain.ParticipationStatus; import corea.participation.repository.ParticipationRepository; import corea.room.domain.Room; -import corea.room.domain.RoomStatus; import corea.room.dto.RoomCreateRequest; import corea.room.dto.RoomParticipantResponses; import corea.room.dto.RoomResponse; @@ -28,9 +26,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; @@ -74,13 +69,32 @@ void create() { assertThat(roomRepository.findAll()).hasSize(1); } + @Test + @DisplayName("방의 매니저가 아니면 수정 시, 예외를 발생합니다.") + void throw_exception_when_update_with_not_manager() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + assertThatThrownBy(() -> roomService.update(-1, RoomFixture.ROOM_UPDATE_REQUEST(response.id()))) + .isInstanceOf(CoreaException.class); + } + + @Test + @DisplayName("존재하지 않는 방이면, 예외를 발생합니다.") + void throw_exception_when_update_with_not_exist_room() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + assertThatThrownBy(() -> roomService.update(manager.getId(), RoomFixture.ROOM_UPDATE_REQUEST(-1))) + .isInstanceOf(CoreaException.class); + } + @Disabled @Test @DisplayName("방을 생성할 때 모집 마감 시간은 현재 시간보다 1시간 이후가 아니라면 예외가 발생한다.") void invalidRecruitmentDeadline() { Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST_WITH_RECRUITMENT_DEADLINE(LocalDateTime.now().plusMinutes(59)); + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST_WITH_RECRUITMENT_DEADLINE(LocalDateTime.now() + .plusMinutes(59)); assertThatThrownBy(() -> roomService.create(manager.getId(), request)) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) @@ -94,7 +108,9 @@ void invalidRecruitmentDeadline() { void invalidReviewDeadline() { Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(LocalDateTime.now().plusHours(2), LocalDateTime.now().plusDays(1)); + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(LocalDateTime.now() + .plusHours(2), LocalDateTime.now() + .plusDays(1)); assertThatThrownBy(() -> roomService.create(manager.getId(), request)) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) @@ -161,8 +177,10 @@ void findParticipatedRooms() { Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); - Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); - Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3))); + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now() + .plusDays(2))); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now() + .plusDays(3))); Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); Long joysonId = joyson.getId(); @@ -182,8 +200,10 @@ void findNonClosedParticipatedRooms() { Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); - Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); - Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3))); + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now() + .plusDays(2))); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now() + .plusDays(3))); Room movinRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(movin)); Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); @@ -210,7 +230,8 @@ void create_participationStatus_manager() { assertAll( () -> assertThat(response.manager()).isEqualTo(manager.getName()), () -> assertThat(participation.isPresent()).isTrue(), - () -> assertThat(participation.get().getStatus()).isEqualTo(ParticipationStatus.MANAGER) + () -> assertThat(participation.get() + .getStatus()).isEqualTo(ParticipationStatus.MANAGER) ); } @@ -252,9 +273,13 @@ void findParticipants() { List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); participationRepository.save(new Participation(room, manager)); - participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); + participationRepository.saveAll(members.stream() + .map(member -> new Participation(room, member, MemberRole.BOTH, 2)) + .toList()); - matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); + matchResultRepository.saveAll(members.stream() + .map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)) + .toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); RoomParticipantResponses participants = roomService.findParticipants(room.getId(), manager.getId()); @@ -277,9 +302,13 @@ void findParticipants_withNoPullRequestParticipants() { List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); - participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); + participationRepository.saveAll(members.stream() + .map(member -> new Participation(room, member, MemberRole.BOTH, 2)) + .toList()); - matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); + matchResultRepository.saveAll(members.stream() + .map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)) + .toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); RoomParticipantResponses participants = assertDoesNotThrow(() -> roomService.findParticipants(room.getId(), manager.getId())); diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingSchedulerTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingSchedulerTest.java new file mode 100644 index 000000000..d7a0caa92 --- /dev/null +++ b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingSchedulerTest.java @@ -0,0 +1,168 @@ +package corea.scheduler.service; + +import config.ServiceTest; +import corea.fixture.MemberFixture; +import corea.fixture.RoomFixture; +import corea.member.domain.Member; +import corea.member.domain.MemberRole; +import corea.member.repository.MemberRepository; +import corea.participation.domain.ParticipationStatus; +import corea.room.domain.Room; +import corea.room.dto.RoomResponse; +import corea.room.repository.RoomRepository; +import corea.scheduler.repository.AutomaticMatchingRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.TaskScheduler; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; + +import static org.assertj.core.api.Assertions.assertThat; + +@ServiceTest +class AutomaticMatchingSchedulerTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TaskScheduler taskScheduler; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private AutomaticMatchingRepository automaticMatchingRepository; + + @Autowired + private AutomaticMatchingExecutor automaticMatchingExecutor; + + private Map> scheduledTasks; + private AutomaticMatchingScheduler automaticMatchingScheduler; + + @BeforeEach + void setup() { + this.scheduledTasks = new HashMap<>(); + this.automaticMatchingScheduler = new AutomaticMatchingScheduler(taskScheduler, automaticMatchingExecutor, scheduledTasks); + } + + @Test + @DisplayName("마감 기한에 맞게 자동 업데이트를 등록한다.") + void updateAtReviewDeadline() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + LocalDateTime reviewDeadline = LocalDateTime.now() + .plusDays(2); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_REVIEW_DEADLINE(manager, reviewDeadline)); + + automaticMatchingScheduler.matchOnRecruitmentDeadline(room); + + assertThat(scheduledTasks.containsKey(room.getId())).isTrue(); + } + + @Test + @DisplayName("예약된 자동 업데이트를 삭제한다.") + void cancel() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + LocalDateTime reviewDeadline = LocalDateTime.now() + .plusDays(2); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_REVIEW_DEADLINE(manager, reviewDeadline)); + automaticMatchingScheduler.matchOnRecruitmentDeadline(room); + ScheduledFuture scheduledFuture = scheduledTasks.get(room.getId()); + + automaticMatchingScheduler.cancel(room.getId()); + + assertThat(scheduledFuture.isCancelled()).isTrue(); + assertThat(scheduledTasks.containsKey(room.getId())).isFalse(); + } + + @Test + @DisplayName("예약된 자동 업데이트를 수정한다.") + void update() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + LocalDateTime reviewDeadline = LocalDateTime.now() + .plusDays(2); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_REVIEW_DEADLINE(manager, reviewDeadline)); + automaticMatchingScheduler.matchOnRecruitmentDeadline(room); + ScheduledFuture task = scheduledTasks.get(room.getId()); + + automaticMatchingScheduler.modifyTask(room); + ScheduledFuture updateTask = scheduledTasks.get(room.getId()); + + assertThat(task.isCancelled()).isTrue(); + assertThat(updateTask.isCancelled()).isFalse(); + } + + +// @Test +// @DisplayName("모집 마감 기한이 되면 매칭을 자동으로 진행한다.") +// void matchOnRecruitmentDeadline() { +// // 현재 시간으로부터 10시간 후로 모집 마감 시간 설정 +// LocalDateTime recruitmentDeadline = LocalDateTime.now() +// .plusHours(10); +// when(roomService.create(anyLong(), any())).thenReturn(getRoomResponse(recruitmentDeadline)); +// +// when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); +// +// RoomResponse response = roomService.create(anyLong(), any()); +// automaticMatchingRepository.save(new AutomaticMatching(response.id(), response.recruitmentDeadline())); +// +// // taskScheduler를 사용하는 메소드 호출 +// +// // taskScheduler.schedule 메소드에 전달된 인자를 캡처하기 위한 ArgumentCaptor 설정 +// ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); +// ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); +// // taskScheduler.schedule 메소드가 호출되었는지 확인하고 전달된 인자 캡처 +// verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); +// Instant scheduledTime = timeCaptor.getValue(); +// runnableCaptor.getValue() +// .run(); +// +// // 예약된 시간이 설정한 모집 마감 시간과 일치하는지 확인 +// assertThat(recruitmentDeadline.atZone(ZoneId.of("Asia/Seoul")) +// .toInstant()).isEqualTo(scheduledTime); +// // automaticMatchingExecutor.execute 메소드가 호출되었는지 확인 +// verify(automaticMatchingExecutor).execute(response.id()); +// } +// +// @Test +// @DisplayName("예약된 자동 매칭을 삭제한다.") +// void cancel() { +// LocalDateTime recruitmentDeadline = LocalDateTime.now() +// .plusHours(10); +// when(roomService.create(anyLong(), any())).thenReturn(getRoomResponse(recruitmentDeadline)); +// ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); +// when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(scheduledFuture); +// +// RoomResponse response = roomService.create(anyLong(), any()); +// roomService.delete(anyLong(), anyLong()); +// automaticMatchingRepository.save(new AutomaticMatching(response.id(), response.recruitmentDeadline())); +// +// verify(scheduledFuture).cancel(true); +// } + + private RoomResponse getRoomResponse(LocalDateTime recruitmentDeadline) { + return new RoomResponse(10, + "title", + "content", + "managerName", + "repolink", + "link", + 2, + List.of(), + 1, + 10, + recruitmentDeadline, + LocalDateTime.now() + .plusDays(3), + ParticipationStatus.PARTICIPATED, + MemberRole.NONE, + "OPEN", + ""); + } +} diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java deleted file mode 100644 index 80ffc87ec..000000000 --- a/backend/src/test/java/corea/scheduler/service/AutomaticMatchingServiceTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package corea.scheduler.service; - -import config.ServiceTest; -import config.TestAsyncConfig; -import corea.member.domain.MemberRole; -import corea.participation.domain.ParticipationStatus; -import corea.room.dto.RoomResponse; -import corea.room.service.RoomService; -import corea.scheduler.domain.AutomaticMatching; -import corea.scheduler.repository.AutomaticMatchingRepository; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.scheduling.TaskScheduler; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.List; -import java.util.concurrent.ScheduledFuture; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -@ServiceTest -@Import(TestAsyncConfig.class) -class AutomaticMatchingServiceTest { - - @Autowired - private AutomaticMatchingService automaticMatchingService; - - @Autowired - private AutomaticMatchingRepository automaticMatchingRepository; - - @MockBean - private RoomService roomService; - - @MockBean - private TaskScheduler taskScheduler; - - @MockBean - private AutomaticMatchingExecutor automaticMatchingExecutor; - - @Test - @DisplayName("모집 마감 기한이 되면 매칭을 자동으로 진행한다.") - void matchOnRecruitmentDeadline() { - // 현재 시간으로부터 10시간 후로 모집 마감 시간 설정 - LocalDateTime recruitmentDeadline = LocalDateTime.now().plusHours(10); - when(roomService.create(anyLong(), any())).thenReturn(getRoomResponse(recruitmentDeadline)); - - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); - - RoomResponse response = roomService.create(anyLong(), any()); - automaticMatchingRepository.save(new AutomaticMatching(response.id(), response.recruitmentDeadline())); - - // taskScheduler를 사용하는 메소드 호출 - automaticMatchingService.matchOnRecruitmentDeadline(response); - - // taskScheduler.schedule 메소드에 전달된 인자를 캡처하기 위한 ArgumentCaptor 설정 - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); - // taskScheduler.schedule 메소드가 호출되었는지 확인하고 전달된 인자 캡처 - verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); - Instant scheduledTime = timeCaptor.getValue(); - runnableCaptor.getValue().run(); - - // 예약된 시간이 설정한 모집 마감 시간과 일치하는지 확인 - assertThat(recruitmentDeadline.atZone(ZoneId.of("Asia/Seoul")).toInstant()).isEqualTo(scheduledTime); - // automaticMatchingExecutor.execute 메소드가 호출되었는지 확인 - verify(automaticMatchingExecutor).execute(response.id()); - } - - @Test - @DisplayName("예약된 자동 매칭을 삭제한다.") - void cancel() { - LocalDateTime recruitmentDeadline = LocalDateTime.now().plusHours(10); - when(roomService.create(anyLong(), any())).thenReturn(getRoomResponse(recruitmentDeadline)); - ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(scheduledFuture); - - RoomResponse response = roomService.create(anyLong(), any()); - automaticMatchingRepository.save(new AutomaticMatching(response.id(), response.recruitmentDeadline())); - - automaticMatchingService.matchOnRecruitmentDeadline(response); - automaticMatchingService.cancel(response.id()); - - verify(scheduledFuture).cancel(true); - } - - private RoomResponse getRoomResponse(LocalDateTime recruitmentDeadline) { - return new RoomResponse(10, - "title", - "content", - "managerName", - "repolink", - "link", - 2, - List.of(), - 1, - 10, - recruitmentDeadline, - LocalDateTime.now().plusDays(3), - ParticipationStatus.PARTICIPATED, - MemberRole.NONE, - "OPEN", - ""); - } -} diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticUpdateSchedulerTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticUpdateSchedulerTest.java new file mode 100644 index 000000000..43a8da865 --- /dev/null +++ b/backend/src/test/java/corea/scheduler/service/AutomaticUpdateSchedulerTest.java @@ -0,0 +1,193 @@ +package corea.scheduler.service; + +import config.ServiceTest; +import corea.fixture.MemberFixture; +import corea.fixture.RoomFixture; +import corea.member.domain.Member; +import corea.member.repository.MemberRepository; +import corea.room.domain.Room; +import corea.room.repository.RoomRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.TaskScheduler; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; + +import static org.assertj.core.api.Assertions.assertThat; + +@ServiceTest +class AutomaticUpdateSchedulerTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TaskScheduler taskScheduler; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private AutomaticUpdateExecutor automaticUpdateExecutor; + + private Map> scheduledTasks; + private AutomaticUpdateScheduler automaticUpdateScheduler; + + @BeforeEach + void setup() { + this.scheduledTasks = new HashMap<>(); + this.automaticUpdateScheduler = new AutomaticUpdateScheduler(taskScheduler, automaticUpdateExecutor, scheduledTasks); + } + + @Test + @DisplayName("마감 기한에 맞게 자동 업데이트를 등록한다.") + void updateAtReviewDeadline() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + LocalDateTime reviewDeadline = LocalDateTime.now() + .plusDays(2); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_REVIEW_DEADLINE(manager, reviewDeadline)); + + automaticUpdateScheduler.updateAtReviewDeadline(room); + + assertThat(scheduledTasks.containsKey(room.getId())).isTrue(); + } + + @Test + @DisplayName("예약된 자동 업데이트를 삭제한다.") + void cancel() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + LocalDateTime reviewDeadline = LocalDateTime.now() + .plusDays(2); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_REVIEW_DEADLINE(manager, reviewDeadline)); + automaticUpdateScheduler.updateAtReviewDeadline(room); + ScheduledFuture scheduledFuture = scheduledTasks.get(room.getId()); + + automaticUpdateScheduler.cancel(room.getId()); + + assertThat(scheduledFuture.isCancelled()).isTrue(); + assertThat(scheduledTasks.containsKey(room.getId())).isFalse(); + } + + @Test + @DisplayName("예약된 자동 업데이트를 수정한다.") + void update() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + LocalDateTime reviewDeadline = LocalDateTime.now() + .plusDays(2); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_REVIEW_DEADLINE(manager, reviewDeadline)); + automaticUpdateScheduler.updateAtReviewDeadline(room); + ScheduledFuture task = scheduledTasks.get(room.getId()); + + automaticUpdateScheduler.modifyTask(room); + ScheduledFuture updateTask = scheduledTasks.get(room.getId()); + + assertThat(task.isCancelled()).isTrue(); + assertThat(updateTask.isCancelled()).isFalse(); + } + +// @Test +// @Transactional +// @DisplayName("리뷰 마감 시간이 되고 리뷰를 작성했다면 리뷰어는 리뷰 작성한 개수가 증가하고 리뷰이는 리뷰 받은 개수가 증가한다.") +// void increaseReviewCount() { +// Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); +// +// LocalDateTime reviewDeadline = LocalDateTime.now() +// .plusDays(2); +// RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); +// +// when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[0]); +// when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); +// +// Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); +// Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); +// MatchResult matchResult = matchResultRepository.save(new MatchResult(response.id(), reviewer, reviewee, "prLink")); +// +// doAnswer(invocation -> { +// matchResult.reviewComplete(); +// return null; +// }).when(reviewService) +// .completeReview(response.id(), reviewer.getId(), reviewee.getId()); +// reviewService.completeReview(response.id(), reviewer.getId(), reviewee.getId()); +// +// ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); +// ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); +// +// verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); +// runnableCaptor.getValue() +// .run(); +// +// assertThat(reviewer.getProfile() +// .getDeliverCount()).isEqualTo(1); +// assertThat(reviewee.getProfile() +// .getReceiveCount()).isEqualTo(1); +// } +// +// @Test +// @Transactional +// @DisplayName("피드백을 작성했다면 방이 종료되었을 때, 피드백 받은 멤버의 정보가 업데이트된다. (피드백 받은 개수 증가, 평균 평점 계산)") +// void updateDevelopFeedbackPoint() { +// Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); +// +// LocalDateTime reviewDeadline = LocalDateTime.now() +// .plusDays(2); +// RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); +// +// when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[0]); +// when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); +// +// Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); +// Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); +// matchResultRepository.save(new MatchResult(response.id(), reviewer, reviewee, "prLink")); +// reviewService.completeReview(response.id(), reviewer.getId(), reviewee.getId()); +// +// developFeedbackRepository.save(new DevelopFeedback(response.id(), reviewer, reviewee, 5, List.of(FeedbackKeyword.EASY_TO_UNDERSTAND_THE_CODE), "", 3)); +// +// ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); +// ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); +// +// verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); +// runnableCaptor.getValue() +// .run(); +// +// Profile profile = reviewee.getProfile(); +// assertThat(profile.getFeedbackCount()).isEqualTo(1); +// assertThat(profile.getAverageRatingValue()).isEqualTo(5); +// } +// +// @Test +// @Transactional +// @DisplayName("피드백을 작성했다면 방이 종료되었을 때, 피드백 받은 멤버의 정보가 업데이트된다. (피드백 받은 개수 증가, 평균 평점 계산)") +// void updateSocialFeedbackPoint() { +// Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); +// +// LocalDateTime reviewDeadline = LocalDateTime.now() +// .plusDays(2); +// RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); +// +// when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[0]); +// when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); +// +// Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); +// Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); +// matchResultRepository.save(new MatchResult(response.id(), reviewer, reviewee, "prLink")); +// reviewService.completeReview(response.id(), reviewer.getId(), reviewee.getId()); +// +// socialFeedbackRepository.save(new SocialFeedback(response.id(), reviewee, reviewer, 5, List.of(FeedbackKeyword.GOOD_AT_EXPLAINING), "")); +// +// ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); +// ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); +// +// verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); +// runnableCaptor.getValue() +// .run(); +// +// Profile profile = reviewer.getProfile(); +// assertThat(profile.getFeedbackCount()).isEqualTo(1); +// assertThat(profile.getAverageRatingValue()).isEqualTo(5); +// } +} diff --git a/backend/src/test/java/corea/scheduler/service/AutomaticUpdateServiceTest.java b/backend/src/test/java/corea/scheduler/service/AutomaticUpdateServiceTest.java deleted file mode 100644 index d8eda04d5..000000000 --- a/backend/src/test/java/corea/scheduler/service/AutomaticUpdateServiceTest.java +++ /dev/null @@ -1,207 +0,0 @@ -package corea.scheduler.service; - -import config.ServiceTest; -import config.TestAsyncConfig; -import corea.auth.dto.GithubPullRequestReview; -import corea.auth.service.GithubOAuthProvider; -import corea.feedback.domain.DevelopFeedback; -import corea.feedback.domain.FeedbackKeyword; -import corea.feedback.domain.SocialFeedback; -import corea.feedback.repository.DevelopFeedbackRepository; -import corea.feedback.repository.SocialFeedbackRepository; -import corea.fixture.MemberFixture; -import corea.fixture.RoomFixture; -import corea.matchresult.domain.MatchResult; -import corea.matchresult.repository.MatchResultRepository; -import corea.member.domain.Member; -import corea.member.domain.Profile; -import corea.member.repository.MemberRepository; -import corea.review.service.ReviewService; -import corea.room.dto.RoomResponse; -import corea.room.service.RoomService; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.List; -import java.util.concurrent.ScheduledFuture; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ServiceTest -@Import(TestAsyncConfig.class) -class AutomaticUpdateServiceTest { - - @Autowired - private AutomaticUpdateService automaticUpdateService; - - @Autowired - private RoomService roomService; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private MatchResultRepository matchResultRepository; - - @Autowired - private DevelopFeedbackRepository developFeedbackRepository; - - @Autowired - private SocialFeedbackRepository socialFeedbackRepository; - - @MockBean - private ReviewService reviewService; - - @MockBean - private TaskScheduler taskScheduler; - - @MockBean - private GithubOAuthProvider githubOAuthProvider; - - @Test - @DisplayName("리뷰 마감 시간이 되면 자동으로 상태를 변경한다.") - void updateAtReviewDeadline() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - LocalDateTime reviewDeadline = LocalDateTime.now().plusDays(2); - RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); - - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); - - automaticUpdateService.updateAtReviewDeadline(response); - - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); - - verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); - Instant scheduledTime = timeCaptor.getValue(); - runnableCaptor.getValue().run(); - - assertThat(reviewDeadline.atZone(ZoneId.of("Asia/Seoul")).toInstant()).isEqualTo(scheduledTime); - } - - @Test - @DisplayName("예약된 자동 업데이트를 삭제한다.") - void cancel() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - LocalDateTime reviewDeadline = LocalDateTime.now().plusDays(2); - RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); - - ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(scheduledFuture); - - automaticUpdateService.updateAtReviewDeadline(response); - automaticUpdateService.cancel(response.id()); - - verify(scheduledFuture).cancel(true); - } - - @Test - @Transactional - @DisplayName("리뷰 마감 시간이 되고 리뷰를 작성했다면 리뷰어는 리뷰 작성한 개수가 증가하고 리뷰이는 리뷰 받은 개수가 증가한다.") - void increaseReviewCount() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - LocalDateTime reviewDeadline = LocalDateTime.now().plusDays(2); - RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); - - when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[0]); - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); - - Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); - MatchResult matchResult = matchResultRepository.save(new MatchResult(response.id(), reviewer, reviewee, "prLink")); - - doAnswer(invocation -> { - matchResult.reviewComplete(); - return null; - }).when(reviewService).completeReview(response.id(), reviewer.getId(), reviewee.getId()); - reviewService.completeReview(response.id(), reviewer.getId(), reviewee.getId()); - automaticUpdateService.updateAtReviewDeadline(response); - - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); - - verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); - runnableCaptor.getValue().run(); - - assertThat(reviewer.getProfile().getDeliverCount()).isEqualTo(1); - assertThat(reviewee.getProfile().getReceiveCount()).isEqualTo(1); - } - - @Test - @Transactional - @DisplayName("피드백을 작성했다면 방이 종료되었을 때, 피드백 받은 멤버의 정보가 업데이트된다. (피드백 받은 개수 증가, 평균 평점 계산)") - void updateDevelopFeedbackPoint() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - LocalDateTime reviewDeadline = LocalDateTime.now().plusDays(2); - RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); - - when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[0]); - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); - - Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); - matchResultRepository.save(new MatchResult(response.id(), reviewer, reviewee, "prLink")); - reviewService.completeReview(response.id(), reviewer.getId(), reviewee.getId()); - - developFeedbackRepository.save(new DevelopFeedback(response.id(), reviewer, reviewee, 5, List.of(FeedbackKeyword.EASY_TO_UNDERSTAND_THE_CODE), "", 3)); - - automaticUpdateService.updateAtReviewDeadline(response); - - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); - - verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); - runnableCaptor.getValue().run(); - - Profile profile = reviewee.getProfile(); - assertThat(profile.getFeedbackCount()).isEqualTo(1); - assertThat(profile.getAverageRatingValue()).isEqualTo(5); - } - - @Test - @Transactional - @DisplayName("피드백을 작성했다면 방이 종료되었을 때, 피드백 받은 멤버의 정보가 업데이트된다. (피드백 받은 개수 증가, 평균 평점 계산)") - void updateSocialFeedbackPoint() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - LocalDateTime reviewDeadline = LocalDateTime.now().plusDays(2); - RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST_WITH_REVIEW_DEADLINE(reviewDeadline)); - - when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[0]); - when(taskScheduler.schedule(any(Runnable.class), any(Instant.class))).thenReturn(mock(ScheduledFuture.class)); - - Member reviewer = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - Member reviewee = memberRepository.save(MemberFixture.MEMBER_PORORO()); - matchResultRepository.save(new MatchResult(response.id(), reviewer, reviewee, "prLink")); - reviewService.completeReview(response.id(), reviewer.getId(), reviewee.getId()); - - socialFeedbackRepository.save(new SocialFeedback(response.id(), reviewee, reviewer, 5, List.of(FeedbackKeyword.GOOD_AT_EXPLAINING), "")); - - automaticUpdateService.updateAtReviewDeadline(response); - - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Instant.class); - - verify(taskScheduler).schedule(runnableCaptor.capture(), timeCaptor.capture()); - runnableCaptor.getValue().run(); - - Profile profile = reviewer.getProfile(); - assertThat(profile.getFeedbackCount()).isEqualTo(1); - assertThat(profile.getAverageRatingValue()).isEqualTo(5); - } -} From c9f98b48496bee1c53de4ef3176325dd348cc7c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:50:54 +0900 Subject: [PATCH 09/31] =?UTF-8?q?[FE]=20=EC=BD=9C=EB=A6=AC=EB=84=A4=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=A0=9C=EA=B3=B5=ED=9B=84=20QA(?= =?UTF-8?q?#597)=20(#602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 모집 마감 -> 종료된 방으로 변경 * fix: 매칭 후 pr 제출 안 해서 실패했을 때 바로 문구 띄우기 * design: 모달 이름에 width 변경 * design: 프로필 드롭다운에서 이름 다 보여지게 하기 * design: 배너 medium일 때 높이 수정 * feat: timeDropdown에서 선택된 시간이 제일 위에 떠있게 하기 * feat: 피드백 모아보기에서 세부 피드백에 scroll 추가 * design: 세부 피드백 높이 지정 * fix: content 길이 길어졌을 때 ... 로 보이게 하기 * design: 피드백 모달 엔터처리, line-height 추가 * feat: 분류 드롭다운으로 선택하게 변경 * feat: 키워드 없을 때 ui 처리 * feat: 방 매칭 실패 시 서버가 준 message 띄우기 * [BE] Room Controller 역할 분리(#595) (#596) * refactor: Room Controller 역할 분리 * refactor: 피드백 반영 --------- Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> * refactor: ALL 타입 추가 * feat: 수정된 api에 맞게 message 타입 추가 * fix: 방 생성 시 키워드 입력을 안했을 때 렌더링 조건 수정 * feat: 바뀐 RoomInfo 데이터 형식에 맞게 storybook 수정 --------- Co-authored-by: jinsil Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> Co-authored-by: 00kang --- frontend/src/@types/roomInfo.ts | 1 + .../common/dropdown/Dropdown.style.ts | 12 ++++---- .../components/common/dropdown/Dropdown.tsx | 28 ++++++++--------- .../common/header/ProfileDropdown.style.ts | 4 ++- .../common/textarea/Textarea.style.ts | 1 + .../common/timeDropdown/TimeDropdown.tsx | 16 +++++++++- .../feedbackCard/FeedbackCard.style.ts | 6 ++-- .../feedbackForm/FeedbackForm.style.ts | 5 ++++ .../main/banner/CyclingClasses.style.ts | 12 +++++--- .../profile/profileCard/ProfileCard.style.ts | 10 +++++-- .../myReviewee/MyReviewee.style.ts | 26 +++++++++++++++- .../roomDetailPage/myReviewee/MyReviewee.tsx | 17 +++++++---- .../myReviewer/MyReviewer.style.ts | 30 +++++++++++++++++-- .../roomDetailPage/myReviewer/MyReviewer.tsx | 17 +++++++---- .../participantList/ParticipantList.style.ts | 6 ++++ .../roomInfoCard/RoomInfoCard.style.ts | 19 ++++++++++-- .../roomInfoCard/RoomInfoCard.tsx | 24 ++++++++------- .../shared/roomCard/RoomCard.stories.tsx | 6 ++-- .../shared/roomCard/RoomCard.style.ts | 10 +++++-- .../components/shared/roomCard/RoomCard.tsx | 14 ++++++--- .../roomCardModal/RoomCardModal.stories.tsx | 6 ++-- .../roomCardModal/RoomCardModal.style.ts | 5 +++- .../shared/roomList/RoomList.stories.tsx | 6 ++-- .../components/shared/roomList/RoomList.tsx | 2 +- .../src/pages/roomCreate/RoomCreatePage.tsx | 28 +++++++++++------ .../src/pages/roomDetail/RoomDetailPage.tsx | 2 +- 26 files changed, 230 insertions(+), 83 deletions(-) diff --git a/frontend/src/@types/roomInfo.ts b/frontend/src/@types/roomInfo.ts index e947b5cb2..cbaea01b8 100644 --- a/frontend/src/@types/roomInfo.ts +++ b/frontend/src/@types/roomInfo.ts @@ -32,6 +32,7 @@ export interface RoomInfo extends BaseRoomInfo { roomStatus: RoomStatus; participationStatus: ParticipationStatus; memberRole: Role; + message: string; } export interface RoomListInfo { diff --git a/frontend/src/components/common/dropdown/Dropdown.style.ts b/frontend/src/components/common/dropdown/Dropdown.style.ts index 53245b002..50a81323a 100644 --- a/frontend/src/components/common/dropdown/Dropdown.style.ts +++ b/frontend/src/components/common/dropdown/Dropdown.style.ts @@ -12,11 +12,10 @@ const dropdown = keyframes` `; export const DropdownContainer = styled.div` + cursor: pointer; position: relative; width: 160px; height: 40px; - - cursor: pointer; `; export const DropdownToggle = styled.div` @@ -31,16 +30,16 @@ export const DropdownToggle = styled.div` font: ${({ theme }) => theme.TEXT.small}; color: ${({ theme }) => theme.COLOR.grey4}; - border: 1px solid ${({ theme }) => theme.COLOR.grey2}; - border-radius: 4px; + border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border-radius: 6px; `; -export const DropdownMenu = styled.div<{ show: boolean }>` +export const DropdownMenu = styled.div` position: absolute; z-index: 1; right: 0; - display: ${({ show }) => (show ? "flex" : "none")}; + display: flex; flex-direction: column; width: 100%; @@ -48,6 +47,7 @@ export const DropdownMenu = styled.div<{ show: boolean }>` background-color: white; border: 1px solid ${({ theme }) => theme.COLOR.grey1}; border-radius: 4px; + animation: ${dropdown} 0.4s ease; `; diff --git a/frontend/src/components/common/dropdown/Dropdown.tsx b/frontend/src/components/common/dropdown/Dropdown.tsx index 443114464..cf84d0c9a 100644 --- a/frontend/src/components/common/dropdown/Dropdown.tsx +++ b/frontend/src/components/common/dropdown/Dropdown.tsx @@ -21,24 +21,24 @@ const Dropdown = ({ dropdownItems, onSelectCategory, selectedCategory }: Dropdow handleToggleDropdown(); }; - const selectedItem = - dropdownItems.find((item) => item.value === selectedCategory) || dropdownItems[0]; - return ( - {selectedItem.text} - {isDropdownOpen ? : } + {dropdownItems.find((item) => item.value === selectedCategory)?.text || "선택해주세요"} + - - - {dropdownItems.map((item) => ( - handleDropdownItemClick(item.value)}> - {item.text} - - ))} - - + + {isDropdownOpen && ( + + + {dropdownItems.map((item) => ( + handleDropdownItemClick(item.value)}> + {item.text} + + ))} + + + )} ); }; diff --git a/frontend/src/components/common/header/ProfileDropdown.style.ts b/frontend/src/components/common/header/ProfileDropdown.style.ts index 307c5c685..5ffa6f122 100644 --- a/frontend/src/components/common/header/ProfileDropdown.style.ts +++ b/frontend/src/components/common/header/ProfileDropdown.style.ts @@ -25,11 +25,12 @@ export const DropdownMenu = styled.div<{ show: boolean }>` min-width: 200px; padding: 1rem; - animation: ${dropdown} 0.4s ease; background-color: white; border-radius: 12px; box-shadow: 0 0 7px 1px ${({ theme }) => theme.COLOR.primary2}; + + animation: ${dropdown} 0.4s ease; `; export const ProfileWrapper = styled.div` @@ -42,6 +43,7 @@ export const ProfileInfo = styled.div` display: flex; flex-direction: column; gap: 0.4rem; + width: fit-content; strong { font: ${({ theme }) => theme.TEXT.medium_bold}; diff --git a/frontend/src/components/common/textarea/Textarea.style.ts b/frontend/src/components/common/textarea/Textarea.style.ts index 3091d8ae6..518a570e7 100644 --- a/frontend/src/components/common/textarea/Textarea.style.ts +++ b/frontend/src/components/common/textarea/Textarea.style.ts @@ -14,6 +14,7 @@ export const StyledTextarea = styled.textarea<{ $error: boolean }>` padding: 0.6rem 1.1rem; font: ${({ theme }) => theme.TEXT.semiSmall}; + line-height: 2.2rem; border: 1px solid ${(props) => (props.$error ? props.theme.COLOR.error : props.theme.COLOR.grey1)}; border-radius: 6px; diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.tsx b/frontend/src/components/common/timeDropdown/TimeDropdown.tsx index af2b209a4..8b9214b78 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.tsx +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.tsx @@ -1,4 +1,4 @@ -import { InputHTMLAttributes } from "react"; +import React, { InputHTMLAttributes, useEffect, useRef } from "react"; import useDropdown from "@/hooks/common/useDropdown"; import * as S from "@/components/common/timeDropdown/TimeDropdown.style"; import { formatTime } from "@/utils/dateFormatter"; @@ -21,6 +21,18 @@ const TimePicker = ({ time: Date; onTimeInputChange: (event: TimeDropdownChangeProps) => void; }) => { + const hourRef = useRef(null); + const minuteRef = useRef(null); + + useEffect(() => { + if (hourRef.current) { + hourRef.current.scrollIntoView({ block: "start" }); + } + if (minuteRef.current) { + minuteRef.current.scrollIntoView({ block: "start" }); + } + }, [time]); + return ( @@ -28,6 +40,7 @@ const TimePicker = ({ { const newTime = new Date(time); newTime.setHours(hour); @@ -44,6 +57,7 @@ const TimePicker = ({ { const newTime = new Date(time); newTime.setMinutes(minute); diff --git a/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts b/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts index 83c81d5a2..5d29f09fa 100644 --- a/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts +++ b/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts @@ -101,12 +101,12 @@ export const FeedbackDetailContainer = styled.div` `; export const FeedbackDetail = styled.p` - overflow: hidden; + overflow: hidden auto; - height: 120px; + height: 172px; font: ${({ theme }) => theme.TEXT.small}; - line-height: 2rem; + line-height: 2.2rem; text-overflow: ellipsis; white-space: break-spaces; `; diff --git a/frontend/src/components/feedback/feedbackForm/FeedbackForm.style.ts b/frontend/src/components/feedback/feedbackForm/FeedbackForm.style.ts index 0933014a4..749ea82e7 100644 --- a/frontend/src/components/feedback/feedbackForm/FeedbackForm.style.ts +++ b/frontend/src/components/feedback/feedbackForm/FeedbackForm.style.ts @@ -33,6 +33,11 @@ export const ModalQuestion = styled.p` export const StyledTextarea = styled.p` display: flex; + width: 100%; + font: ${({ theme }) => theme.TEXT.small}; + line-height: 2.2rem; + overflow-wrap: break-word; + white-space: pre-wrap; `; diff --git a/frontend/src/components/main/banner/CyclingClasses.style.ts b/frontend/src/components/main/banner/CyclingClasses.style.ts index 08bb7edbb..2453f29b6 100644 --- a/frontend/src/components/main/banner/CyclingClasses.style.ts +++ b/frontend/src/components/main/banner/CyclingClasses.style.ts @@ -3,7 +3,11 @@ import media from "@/styles/media"; export const CyclingContainer = styled.div` overflow: hidden; - height: 54px; + height: 60px; + + ${media.medium` + height: 42px; + `} `; export const CyclingList = styled.ul` @@ -13,9 +17,9 @@ export const CyclingList = styled.ul` flex-direction: column; align-items: flex-end; - width: 160px; + width: 180px; height: 52px; - padding: 0.2rem; + padding: 1rem; font-family: "Do Hyeon", sans-serif; font-size: 8rem; @@ -42,7 +46,7 @@ export const CyclingList = styled.ul` } ${media.medium` - width: 120px; + width: 150px; height: 40px; font-size: 6.4rem; `} diff --git a/frontend/src/components/profile/profileCard/ProfileCard.style.ts b/frontend/src/components/profile/profileCard/ProfileCard.style.ts index b7a0b74de..82193b116 100644 --- a/frontend/src/components/profile/profileCard/ProfileCard.style.ts +++ b/frontend/src/components/profile/profileCard/ProfileCard.style.ts @@ -1,11 +1,10 @@ -import { decorators } from "./../../../../.storybook/preview"; import styled from "styled-components"; export const ProfileCardContainer = styled.div` width: 100%; + padding: 1rem; border: 1px solid ${({ theme }) => theme.COLOR.grey1}; border-radius: 20px; - padding: 1rem; `; export const ProfileTitle = styled.div` @@ -30,10 +29,17 @@ export const ProfileWrapper = styled.div` `; export const ProfileNickname = styled.div` + overflow: hidden; + + max-width: 108px; + height: 24px; + font: ${({ theme }) => theme.TEXT.medium_bold}; color: ${({ theme }) => theme.COLOR.grey3}; text-align: center; text-decoration: underline; + text-overflow: ellipsis; + white-space: nowrap; &:hover { color: ${({ theme }) => theme.COLOR.primary2}; diff --git a/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts index 63f61460e..a694d1cb1 100644 --- a/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts +++ b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts @@ -43,6 +43,30 @@ export const MyRevieweeContent = styled.span` } `; +export const MyRevieweeId = styled.span` + overflow: hidden; + display: block; + + box-sizing: border-box; + width: 100%; + max-width: 100px; + height: 40px; + + font: ${({ theme }) => theme.TEXT.semiSmall}; + line-height: 40px; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + + ${media.medium` + max-width: 120px; + `} + + ${media.large` + max-width: 100%; + `} +`; + export const PRLink = styled.a` cursor: pointer; @@ -63,7 +87,7 @@ export const PRLink = styled.a` export const IconWrapper = styled.span` ${media.small` display: none; -`} + `} `; export const GuidanceWrapper = styled.div` diff --git a/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx index 2226f12f8..86df259ee 100644 --- a/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx +++ b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx @@ -96,15 +96,20 @@ const MyReviewee = ({ roomInfo }: MyReviewerProps) => { ); } + // 매칭 후 PR 제출 안 해서 매칭 실패했을 때 보여줄 화면 + if (roomInfo.participationStatus === "PULL_REQUEST_NOT_SUBMITTED") { + return ( + +

{MESSAGES.GUIDANCE.PULL_REQUEST_NOT_SUBMITTED}

+
+ ); + } + // 방 종료 후 실패했을 때 보여줄 화면 if (roomInfo.roomStatus === "CLOSE" && revieweeData.length === 0) { return ( -

- {roomInfo.participationStatus === "PULL_REQUEST_NOT_SUBMITTED" - ? MESSAGES.GUIDANCE.PULL_REQUEST_NOT_SUBMITTED - : MESSAGES.GUIDANCE.FAIL_MATCHED} -

+

{MESSAGES.GUIDANCE.FAIL_MATCHED}

); } @@ -138,7 +143,7 @@ const MyReviewee = ({ roomInfo }: MyReviewerProps) => { {revieweeData?.map((reviewee) => ( - {reviewee.username} + {reviewee.username} diff --git a/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts index 7e243af19..d2dc190a4 100644 --- a/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts +++ b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts @@ -12,9 +12,10 @@ export const MyReviewerWrapper = styled.div` display: grid; grid-template-columns: 1fr 1fr 1fr; place-items: center center; - padding: 0.7rem 1rem; - height: 40px; + box-sizing: content-box; + height: 40px; + padding: 0.7rem 1rem; &:not(:last-child) { border-bottom: 1px solid ${({ theme }) => theme.COLOR.grey1}; @@ -32,6 +33,7 @@ export const MyReviewerContent = styled.span` display: flex; align-items: center; justify-content: center; + height: 40px; font: ${({ theme }) => theme.TEXT.semiSmall}; @@ -43,6 +45,30 @@ export const MyReviewerContent = styled.span` } `; +export const MyReviewerId = styled.span` + overflow: hidden; + display: block; + + box-sizing: border-box; + width: 100%; + max-width: 100px; + height: 40px; + + font: ${({ theme }) => theme.TEXT.semiSmall}; + line-height: 40px; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + + ${media.medium` + max-width: 120px; + `} + + ${media.large` + max-width: 100%; + `} +`; + export const PRLink = styled.a` cursor: pointer; diff --git a/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx index 48c57d644..06086dc0c 100644 --- a/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx +++ b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx @@ -84,15 +84,20 @@ const MyReviewer = ({ roomInfo }: MyReviewerProps) => { ); } + // 매칭 후 PR 제출 안 해서 매칭 실패했을 때 보여줄 화면 + if (roomInfo.participationStatus === "PULL_REQUEST_NOT_SUBMITTED") { + return ( + +

{MESSAGES.GUIDANCE.PULL_REQUEST_NOT_SUBMITTED}

+
+ ); + } + // 방 종료 후 실패했을 때 보여줄 화면 if (roomInfo.roomStatus === "CLOSE" && reviewerData.length === 0) { return ( -

- {roomInfo.participationStatus === "PULL_REQUEST_NOT_SUBMITTED" - ? MESSAGES.GUIDANCE.PULL_REQUEST_NOT_SUBMITTED - : MESSAGES.GUIDANCE.FAIL_MATCHED} -

+

{MESSAGES.GUIDANCE.FAIL_MATCHED}

); } @@ -127,7 +132,7 @@ const MyReviewer = ({ roomInfo }: MyReviewerProps) => { return ( - {reviewer.username} + {reviewer.username} {reviewer.link.length !== 0 ? ( diff --git a/frontend/src/components/roomDetailPage/participantList/ParticipantList.style.ts b/frontend/src/components/roomDetailPage/participantList/ParticipantList.style.ts index 06d484495..ac4fefdd6 100644 --- a/frontend/src/components/roomDetailPage/participantList/ParticipantList.style.ts +++ b/frontend/src/components/roomDetailPage/participantList/ParticipantList.style.ts @@ -68,8 +68,14 @@ export const ProfileWrapper = styled.div` `; export const ProfileNickname = styled.div` + overflow: hidden; + + max-width: 80rem; + font: ${({ theme }) => theme.TEXT.small}; text-align: center; + text-overflow: ellipsis; + white-space: nowrap; `; export const PRLink = styled.a` diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts index e97b3e300..e20f61574 100644 --- a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts @@ -94,6 +94,11 @@ export const RoomTagBox = styled.div` gap: 1rem; `; +export const NoKeywordText = styled.span` + font: ${({ theme }) => theme.TEXT.semiSmall}; + color: ${({ theme }) => theme.COLOR.grey2}; +`; + export const RoomContentSmall = styled.span` display: flex; gap: 1rem; @@ -102,13 +107,22 @@ export const RoomContentSmall = styled.span` font: ${({ theme }) => theme.TEXT.small_bold}; line-height: 2rem; color: ${({ theme }) => theme.COLOR.black}; - white-space: pre-line; span { font: ${({ theme }) => theme.TEXT.small}; color: ${({ theme }) => theme.COLOR.grey4}; } + span#githubid { + overflow: hidden; + + max-width: 210px; + + font: ${({ theme }) => theme.TEXT.small_bold}; + text-overflow: ellipsis; + white-space: nowrap; + } + div { display: flex; flex-direction: row; @@ -123,8 +137,7 @@ export const RoomContentSmall = styled.span` export const ContentLineBreak = styled.div` display: flex; - flex-direction: row; - flex-wrap: wrap; + flex-flow: row wrap; `; export const DateTimeText = styled.p` diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx index 8dcc7f7cd..04ac35649 100644 --- a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx @@ -25,15 +25,19 @@ const RoomInfoCard = ({ roomInfo }: { roomInfo: RoomInfo }) => { - {roomInfo.keywords.map((keyword) => ( - {roomInfo.content} @@ -42,7 +46,7 @@ const RoomInfoCard = ({ roomInfo }: { roomInfo: RoomInfo }) => { 방 생성자 : - {roomInfo.manager} + {roomInfo.manager} diff --git a/frontend/src/components/shared/roomCard/RoomCard.stories.tsx b/frontend/src/components/shared/roomCard/RoomCard.stories.tsx index 77adee688..3fae3ca02 100644 --- a/frontend/src/components/shared/roomCard/RoomCard.stories.tsx +++ b/frontend/src/components/shared/roomCard/RoomCard.stories.tsx @@ -5,12 +5,14 @@ import roomInfo from "@/mocks/mockResponse/roomInfo.json"; const sampleRoomList = { ...roomInfo, - roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS", + roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS" | "FAIL", participationStatus: roomInfo.participationStatus as | "NOT_PARTICIPATED" | "PARTICIPATED" - | "MANAGER", + | "MANAGER" + | "PULL_REQUEST_NOT_SUBMITTED", memberRole: roomInfo.memberRole as "BOTH" | "REVIEWER" | "REVIEWEE" | "NONE", + message: "FAIL시 오류 메시지", } satisfies RoomInfo; const meta = { diff --git a/frontend/src/components/shared/roomCard/RoomCard.style.ts b/frontend/src/components/shared/roomCard/RoomCard.style.ts index d4b6596b4..662b650d7 100644 --- a/frontend/src/components/shared/roomCard/RoomCard.style.ts +++ b/frontend/src/components/shared/roomCard/RoomCard.style.ts @@ -78,13 +78,14 @@ export const RoomTitle = styled.h2` export const KeywordsContainer = styled.div` display: flex; gap: 2px; + height: 33px; `; export const KeywordWrapper = styled.div` display: flex; - flex-direction: row; + flex-wrap: wrap; gap: 0.5rem; - align-items: center; + align-items: flex-start; `; export const KeywordText = styled.span` @@ -92,6 +93,11 @@ export const KeywordText = styled.span` color: ${({ theme }) => theme.COLOR.grey3}; `; +export const NoKeywordText = styled.span` + font: ${({ theme }) => theme.TEXT.semiSmall}; + color: ${({ theme }) => theme.COLOR.grey2}; +`; + export const EtcContainer = styled.div` display: flex; justify-content: space-between; diff --git a/frontend/src/components/shared/roomCard/RoomCard.tsx b/frontend/src/components/shared/roomCard/RoomCard.tsx index f1ce5d345..5bca335bc 100644 --- a/frontend/src/components/shared/roomCard/RoomCard.tsx +++ b/frontend/src/components/shared/roomCard/RoomCard.tsx @@ -46,7 +46,9 @@ interface RoomCardProps { const RoomCard = React.memo(({ roomInfo }: RoomCardProps) => { const { isModalOpen, handleOpenModal, handleCloseModal } = useModal(); - const displayedKeywords = roomInfo.keywords.slice(0, MAX_KEYWORDS); + const displayedKeywords = roomInfo.keywords + .filter((keyword) => keyword !== "") + .slice(0, MAX_KEYWORDS); return ( <> @@ -63,9 +65,13 @@ const RoomCard = React.memo(({ roomInfo }: RoomCardProps) => { - {displayedKeywords.map((keyword) => ( - #{keyword} - ))} + {displayedKeywords.length === 0 ? ( + 지정된 키워드 없음 + ) : ( + displayedKeywords.map((keyword) => ( + #{keyword} + )) + )} diff --git a/frontend/src/components/shared/roomCardModal/RoomCardModal.stories.tsx b/frontend/src/components/shared/roomCardModal/RoomCardModal.stories.tsx index 84486d52a..88229e5a9 100644 --- a/frontend/src/components/shared/roomCardModal/RoomCardModal.stories.tsx +++ b/frontend/src/components/shared/roomCardModal/RoomCardModal.stories.tsx @@ -5,12 +5,14 @@ import roomInfo from "@/mocks/mockResponse/roomInfo.json"; const sampleRoomList = { ...roomInfo, - roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS", + roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS" | "FAIL", participationStatus: roomInfo.participationStatus as | "NOT_PARTICIPATED" | "PARTICIPATED" - | "MANAGER", + | "MANAGER" + | "PULL_REQUEST_NOT_SUBMITTED", memberRole: roomInfo.memberRole as "BOTH" | "REVIEWER" | "REVIEWEE" | "NONE", + message: "FAIL시 오류 메시지", } satisfies RoomInfo; const meta: Meta = { diff --git a/frontend/src/components/shared/roomCardModal/RoomCardModal.style.ts b/frontend/src/components/shared/roomCardModal/RoomCardModal.style.ts index f66998b63..f6d05cbb2 100644 --- a/frontend/src/components/shared/roomCardModal/RoomCardModal.style.ts +++ b/frontend/src/components/shared/roomCardModal/RoomCardModal.style.ts @@ -75,7 +75,10 @@ export const ProfileContainer = styled.div` } span { - width: 168px; + overflow: hidden; + + max-width: 168px; + font: ${({ theme }) => theme.TEXT.small}; text-overflow: ellipsis; white-space: nowrap; diff --git a/frontend/src/components/shared/roomList/RoomList.stories.tsx b/frontend/src/components/shared/roomList/RoomList.stories.tsx index 4962cf576..517421cfa 100644 --- a/frontend/src/components/shared/roomList/RoomList.stories.tsx +++ b/frontend/src/components/shared/roomList/RoomList.stories.tsx @@ -5,13 +5,15 @@ import roomInfos from "@/mocks/mockResponse/roomInfos.json"; const sampleRoomList = roomInfos.rooms.map((roomInfo) => ({ ...roomInfo, - roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS", + roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS" | "FAIL", participationStatus: roomInfo.participationStatus as | "NOT_PARTICIPATED" | "PARTICIPATED" - | "MANAGER", + | "MANAGER" + | "PULL_REQUEST_NOT_SUBMITTED", memberRole: roomInfo.memberRole as "BOTH" | "REVIEWER" | "REVIEWEE" | "NONE", + message: "FAIL시 오류 메시지", })) satisfies RoomInfo[]; const meta = { diff --git a/frontend/src/components/shared/roomList/RoomList.tsx b/frontend/src/components/shared/roomList/RoomList.tsx index dd6f4e26c..4b58e269a 100644 --- a/frontend/src/components/shared/roomList/RoomList.tsx +++ b/frontend/src/components/shared/roomList/RoomList.tsx @@ -19,7 +19,7 @@ const RoomEmptyText = { participated: "참여한 방이 없습니다.", progress: "진행 중인 방이 없습니다.", opened: "모집 중인 방이 없습니다.", - closed: "모집 마감된 방이 없습니다.", + closed: "종료된 방이 없습니다.", }; const RoomList = ({ diff --git a/frontend/src/pages/roomCreate/RoomCreatePage.tsx b/frontend/src/pages/roomCreate/RoomCreatePage.tsx index 1cdd3d550..571952981 100644 --- a/frontend/src/pages/roomCreate/RoomCreatePage.tsx +++ b/frontend/src/pages/roomCreate/RoomCreatePage.tsx @@ -5,12 +5,13 @@ import useMutateRoom from "@/hooks/mutations/useMutateRoom"; import Button from "@/components/common/button/Button"; import CalendarDropdown from "@/components/common/calendarDropdown/CalendarDropdown"; import ContentSection from "@/components/common/contentSection/ContentSection"; +import Dropdown, { DropdownItem } from "@/components/common/dropdown/Dropdown"; import { Input } from "@/components/common/input/Input"; import ConfirmModal from "@/components/common/modal/confirmModal/ConfirmModal"; import { Textarea } from "@/components/common/textarea/Textarea"; import { TimeDropdown } from "@/components/common/timeDropdown/TimeDropdown"; import * as S from "@/pages/roomCreate/RoomCreatePage.style"; -import { CreateRoomInfo } from "@/@types/roomInfo"; +import { Classification, CreateRoomInfo } from "@/@types/roomInfo"; import MESSAGES from "@/constants/message"; import { formatCombinedDateTime } from "@/utils/dateFormatter"; @@ -27,6 +28,12 @@ const initialFormState: CreateRoomInfo = { classification: "", }; +const dropdownItems: DropdownItem[] = [ + { text: "안드로이드", value: "ANDROID" }, + { text: "백엔드", value: "BACKEND" }, + { text: "프론트엔드", value: "FRONTEND" }, +]; + const RoomCreatePage = () => { const navigate = useNavigate(); @@ -110,7 +117,7 @@ const RoomCreatePage = () => { - 제목*필수입력 + 제목 *필수입력 { - 분류(FRONTEND, BACKEND, ANDROID) *필수입력 + 분류 *필수입력 - + setFormState((prevState) => ({ + ...prevState, + classification: value as Classification, + })) + } /> diff --git a/frontend/src/pages/roomDetail/RoomDetailPage.tsx b/frontend/src/pages/roomDetail/RoomDetailPage.tsx index cbfb1517a..3b0b4ba16 100644 --- a/frontend/src/pages/roomDetail/RoomDetailPage.tsx +++ b/frontend/src/pages/roomDetail/RoomDetailPage.tsx @@ -81,7 +81,7 @@ const RoomDetailPage = () => { 매칭 실패시 이미지 -

현재 참여 중인 인원이 충분하지 않아 프로세스가 일찍 종료될 예정입니다.

+

{roomInfo.message}

); From de8d80adcddc22f3f613462606d5236f95d4f17d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:55:22 +0900 Subject: [PATCH 10/31] =?UTF-8?q?[BE]=20=EC=B0=B8=EC=97=AC=ED=96=88?= =?UTF-8?q?=EB=8D=98=20=EB=B0=A9=EC=9D=B4=20=EC=A2=85=EB=A3=8C=20=EB=90=9C?= =?UTF-8?q?=20=ED=9B=84,=20=EC=A2=85=EB=A3=8C=20=ED=83=AD=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=95=88=EB=B3=B4=EC=9D=B4=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0(#607)=20(#611)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 참여한 방을 조회하는 기능 수정 * refactor: 방을 조회하는 기능 메서드 분리 * refactor: 방을 조회 메서드 클래스 분리 * test: Nested 어노테이션을 통한 RoomServiceTest 관심사 분리 * feat: 참여했던 방이 종료되면 종료 탭에서 보이도록 하는 기능 구현 * feat: 종료 탭에서 자신이 참여했던 방도 나타내는 기능 구현 * test: 테스트 수정 * refactor: 조회 쿼리 수정 * refactor: 피드백 반영 * refactor: 변수명 변경 --------- Co-authored-by: gyungchan Jo --- .../src/main/java/corea/DataInitializer.java | 3 +- .../participation/domain/Participation.java | 4 + .../corea/room/controller/RoomController.java | 6 - .../RoomControllerSpecification.java | 15 +- .../controller/RoomInquiryController.java | 8 + .../RoomInquiryControllerSpecification.java | 11 + .../java/corea/room/dto/RoomResponses.java | 17 +- .../corea/room/repository/RoomRepository.java | 24 +- .../room/service/RoomInquiryService.java | 68 +++- .../java/corea/room/service/RoomService.java | 13 - .../test/java/corea/fixture/RoomFixture.java | 18 + .../room/acceptance/RoomAcceptanceTest.java | 50 ++- .../room/repository/RoomRepositoryTest.java | 36 +- .../room/service/RoomInquiryServiceTest.java | 101 ++++- .../corea/room/service/RoomServiceTest.java | 353 +++++++----------- 15 files changed, 416 insertions(+), 311 deletions(-) diff --git a/backend/src/main/java/corea/DataInitializer.java b/backend/src/main/java/corea/DataInitializer.java index 92b4c95ce..07d684e80 100644 --- a/backend/src/main/java/corea/DataInitializer.java +++ b/backend/src/main/java/corea/DataInitializer.java @@ -119,7 +119,7 @@ public void run(ApplicationArguments args) { LocalDateTime.of(2024, 12, 25, 12, 30), LocalDateTime.of(2025, 1, 3, 12, 0), RoomClassification.BACKEND, RoomStatus.OPEN)); - roomRepository.save( + Room closedRoom = roomRepository.save( new Room("방 제목 10", "방 설명 10", 3, null, null, List.of("TDD", "클린코드"), 1, 20, member1, @@ -204,6 +204,7 @@ public void run(ApplicationArguments args) { participationRepository.save(new Participation(room7, member1, MemberRole.BOTH, room7.getMatchingSize())); participationRepository.save(new Participation(room7, member2, MemberRole.BOTH, room7.getMatchingSize())); + participationRepository.save(new Participation(closedRoom, member1, MemberRole.BOTH, closedRoom.getMatchingSize())); participationRepository.save(new Participation(roomProgress, member1, MemberRole.BOTH, roomProgress.getMatchingSize())); } } diff --git a/backend/src/main/java/corea/participation/domain/Participation.java b/backend/src/main/java/corea/participation/domain/Participation.java index 18f858207..29e9e3cba 100644 --- a/backend/src/main/java/corea/participation/domain/Participation.java +++ b/backend/src/main/java/corea/participation/domain/Participation.java @@ -78,6 +78,10 @@ public boolean isPullRequestNotSubmitted() { return status.isPullRequestNotSubmitted(); } + public boolean isParticipatedRoom(Room room) { + return this.room == room; + } + public long getRoomsId() { return room.getId(); } diff --git a/backend/src/main/java/corea/room/controller/RoomController.java b/backend/src/main/java/corea/room/controller/RoomController.java index b9ae9cc30..7a9dc5c98 100644 --- a/backend/src/main/java/corea/room/controller/RoomController.java +++ b/backend/src/main/java/corea/room/controller/RoomController.java @@ -46,12 +46,6 @@ public ResponseEntity participants(@PathVariable long return ResponseEntity.ok(response); } - @GetMapping("/participated") - public ResponseEntity participatedRooms(@LoginMember AuthInfo authInfo) { - RoomResponses response = roomService.findParticipatedRooms(authInfo.getId()); - return ResponseEntity.ok(response); - } - @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable long id, @LoginMember AuthInfo authInfo) { roomService.delete(id, authInfo.getId()); diff --git a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java index f29a4fc63..b41942938 100644 --- a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java +++ b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java @@ -3,8 +3,10 @@ import corea.auth.domain.AuthInfo; import corea.exception.ExceptionType; import corea.global.annotation.ApiErrorResponses; -import corea.matchresult.dto.MatchResultResponses; -import corea.room.dto.*; +import corea.room.dto.RoomCreateRequest; +import corea.room.dto.RoomParticipantResponses; +import corea.room.dto.RoomResponse; +import corea.room.dto.RoomUpdateRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -57,15 +59,6 @@ ResponseEntity participants(@Parameter(description = " long id, AuthInfo authInfo); - @Operation(summary = "참여 중인 방 정보를 반환합니다..", - description = "해당 멤버가 참여 중인 방들의 정보를 리뷰 마감일이 임박한 순으로 정렬해 반환합니다.
" + - "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + - "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + - "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + - "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + - "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - ResponseEntity participatedRooms(AuthInfo authInfo); - @Operation(summary = "방을 삭제합니다.", description = "이미 생성되어 있는 방을 삭제합니다.
" + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + diff --git a/backend/src/main/java/corea/room/controller/RoomInquiryController.java b/backend/src/main/java/corea/room/controller/RoomInquiryController.java index 6cc3d1e73..c37757bb1 100644 --- a/backend/src/main/java/corea/room/controller/RoomInquiryController.java +++ b/backend/src/main/java/corea/room/controller/RoomInquiryController.java @@ -1,6 +1,7 @@ package corea.room.controller; import corea.auth.annotation.AccessedMember; +import corea.auth.annotation.LoginMember; import corea.auth.domain.AuthInfo; import corea.room.domain.RoomStatus; import corea.room.dto.RoomResponses; @@ -42,4 +43,11 @@ public ResponseEntity closedRooms(@AccessedMember AuthInfo authIn RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(authInfo.getId(), page, expression, RoomStatus.CLOSE); return ResponseEntity.ok(response); } + + @GetMapping("/participated") + public ResponseEntity participatedRooms(@LoginMember AuthInfo authInfo, + @RequestParam(defaultValue = "false") boolean includeClosed) { + RoomResponses response = roomInquiryService.findParticipatedRooms(authInfo.getId(), includeClosed); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java b/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java index 14cc38b5b..8cd2df73d 100644 --- a/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java +++ b/backend/src/main/java/corea/room/controller/RoomInquiryControllerSpecification.java @@ -57,4 +57,15 @@ ResponseEntity closedRooms(AuthInfo authInfo, @Parameter(description = "방 분야", example = "FE") String expression); + + @Operation(summary = "참여 중인 방 정보를 반환합니다..", + description = "해당 멤버가 참여 중인 방들의 정보를 리뷰 마감일이 임박한 순으로 정렬해 반환합니다.
" + + "요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " + + "이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다.
" + + "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") + ResponseEntity participatedRooms(AuthInfo authInfo, + @Parameter(description = "종료된 방 포함 여부", example = "false") + boolean includeClosed); } diff --git a/backend/src/main/java/corea/room/dto/RoomResponses.java b/backend/src/main/java/corea/room/dto/RoomResponses.java index 80c3ffec9..5f6abfa66 100644 --- a/backend/src/main/java/corea/room/dto/RoomResponses.java +++ b/backend/src/main/java/corea/room/dto/RoomResponses.java @@ -1,16 +1,9 @@ package corea.room.dto; -import corea.member.domain.MemberRole; -import corea.participation.domain.ParticipationStatus; -import corea.room.domain.Room; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.data.domain.Page; import java.util.List; -import static java.util.stream.Collectors.collectingAndThen; -import static java.util.stream.Collectors.toList; - @Schema(description = "방들의 정보 응답") public record RoomResponses(@Schema(description = "방 정보들") List rooms, @@ -22,13 +15,7 @@ public record RoomResponses(@Schema(description = "방 정보들") int pageNumber ) { - public static RoomResponses of(List rooms, MemberRole role, ParticipationStatus participationStatus, boolean isLastPage, int pageNumber) { - return rooms.stream() - .map(room -> RoomResponse.of(room, role, participationStatus)) - .collect(collectingAndThen(toList(), responses -> new RoomResponses(responses, isLastPage, pageNumber))); - } - - public static RoomResponses of(Page roomsWithPage, MemberRole role, ParticipationStatus participationStatus, int pageNumber) { - return of(roomsWithPage.getContent(), role, participationStatus, roomsWithPage.isLast(), pageNumber); + public static RoomResponses of(List rooms, boolean isLastPage, int pageNumber) { + return new RoomResponses(rooms, isLastPage, pageNumber); } } diff --git a/backend/src/main/java/corea/room/repository/RoomRepository.java b/backend/src/main/java/corea/room/repository/RoomRepository.java index 31e631ad2..00d276770 100644 --- a/backend/src/main/java/corea/room/repository/RoomRepository.java +++ b/backend/src/main/java/corea/room/repository/RoomRepository.java @@ -4,36 +4,16 @@ import corea.room.domain.RoomClassification; import corea.room.domain.RoomStatus; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import java.util.List; public interface RoomRepository extends JpaRepository { - @Query(""" - SELECT r FROM Room r - LEFT JOIN Participation p - ON r = p.room AND p.member.id = :memberId - WHERE p.id IS NULL AND r.status = :status AND r.manager.id <> :memberId - ORDER BY r.recruitmentDeadline ASC - """) - Page findAllByMemberAndStatus(long memberId, RoomStatus status, PageRequest pageRequest); + Page findAllByStatusOrderByRecruitmentDeadline(RoomStatus status, Pageable pageable); - @Query(""" - SELECT r FROM Room r - LEFT JOIN Participation p - ON r = p.room AND p.member.id = :memberId - WHERE p.id IS NULL AND r.classification = :classification AND r.status = :status AND r.manager.id <> :memberId - ORDER BY r.recruitmentDeadline ASC - """) - Page findAllByMemberAndClassificationAndStatus(long memberId, RoomClassification classification, RoomStatus status, Pageable pageable); - - Page findAllByStatusOrderByRecruitmentDeadlineAsc(RoomStatus status, PageRequest pageRequest); - - Page findAllByClassificationAndStatusOrderByRecruitmentDeadlineAsc(RoomClassification classification, RoomStatus status, Pageable pageable); + Page findAllByClassificationAndStatusOrderByRecruitmentDeadline(RoomClassification classification, RoomStatus status, Pageable pageable); List findAllByIdInOrderByReviewDeadlineAsc(List ids); } diff --git a/backend/src/main/java/corea/room/service/RoomInquiryService.java b/backend/src/main/java/corea/room/service/RoomInquiryService.java index 0c93d2413..0edb0035e 100644 --- a/backend/src/main/java/corea/room/service/RoomInquiryService.java +++ b/backend/src/main/java/corea/room/service/RoomInquiryService.java @@ -1,10 +1,13 @@ package corea.room.service; import corea.member.domain.MemberRole; +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; import corea.room.domain.RoomStatus; +import corea.room.dto.RoomResponse; import corea.room.dto.RoomResponses; import corea.room.repository.RoomRepository; import lombok.RequiredArgsConstructor; @@ -14,27 +17,78 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class RoomInquiryService { + private static final int PAGE_DISPLAY_SIZE = 8; + private final RoomRepository roomRepository; + private final ParticipationRepository participationRepository; public RoomResponses findRoomsWithRoomStatus(long memberId, int pageNumber, String expression, RoomStatus roomStatus) { - RoomClassification classification = RoomClassification.from(expression); - return getRoomResponses(memberId, pageNumber, classification, roomStatus); + Page roomsWithPage = getPaginatedRooms(pageNumber, expression, roomStatus); + List roomResponses = getRoomResponses(roomsWithPage.getContent(), memberId); + + return RoomResponses.of(roomResponses, roomsWithPage.isLast(), pageNumber); } - private RoomResponses getRoomResponses(long memberId, int pageNumber, RoomClassification classification, RoomStatus status) { + private Page getPaginatedRooms(int pageNumber, String expression, RoomStatus status) { + RoomClassification classification = RoomClassification.from(expression); PageRequest pageRequest = PageRequest.of(pageNumber, PAGE_DISPLAY_SIZE); if (classification.isAll()) { - Page roomsWithPage = roomRepository.findAllByMemberAndStatus(memberId, status, pageRequest); - return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber); + return roomRepository.findAllByStatusOrderByRecruitmentDeadline(status, pageRequest); + } + return roomRepository.findAllByClassificationAndStatusOrderByRecruitmentDeadline(classification, status, pageRequest); + } + + private List getRoomResponses(List rooms, long memberId) { + List participations = participationRepository.findAllByMemberId(memberId); + + return rooms.stream() + .map(room -> getRoomResponse(participations, room)) + .toList(); + } + + private RoomResponse getRoomResponse(List participations, Room room) { + return participations.stream() + .filter(participation -> participation.isParticipatedRoom(room)) + .findFirst() + .map(participation -> RoomResponse.of(room, participation)) + .orElseGet(() -> RoomResponse.of(room, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED)); + } + + public RoomResponses findParticipatedRooms(long memberId, boolean includeClosed) { + List rooms = findFilteredParticipatedRooms(memberId, includeClosed); + List roomResponses = getRoomResponses(rooms, memberId); + + return RoomResponses.of(roomResponses, true, 0); + } + + private List findFilteredParticipatedRooms(long memberId, boolean includeClosed) { + List rooms = findAllParticipatedRooms(memberId); + + if (includeClosed) { + return rooms; } - Page roomsWithPage = roomRepository.findAllByMemberAndClassificationAndStatus(memberId, classification, status, pageRequest); - return RoomResponses.of(roomsWithPage, MemberRole.NONE, ParticipationStatus.NOT_PARTICIPATED, pageNumber); + return filterOutClosedRooms(rooms); + } + + private List findAllParticipatedRooms(long memberId) { + return participationRepository.findAllByMemberId(memberId) + .stream() + .map(Participation::getRoom) + .toList(); + } + + private List filterOutClosedRooms(List rooms) { + return rooms.stream() + .filter(Room::isNotClosed) + .toList(); } } diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index f0a856032..44b07f643 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -102,19 +102,6 @@ private RoomResponse createRoomResponseWithParticipation(Room room, Participatio .orElseGet(() -> RoomResponse.of(room, participation)); } - public RoomResponses findParticipatedRooms(long memberId) { - List rooms = findNonClosedParticipatedRooms(memberId); - return RoomResponses.of(rooms, MemberRole.NONE, ParticipationStatus.PARTICIPATED, true, 0); - } - - private List findNonClosedParticipatedRooms(long memberId) { - return participationRepository.findAllByMemberId(memberId) - .stream() - .map(Participation::getRoom) - .filter(Room::isNotClosed) - .toList(); - } - @Transactional public void delete(long roomId, long memberId) { Room room = getRoom(roomId); diff --git a/backend/src/test/java/corea/fixture/RoomFixture.java b/backend/src/test/java/corea/fixture/RoomFixture.java index 7192ba214..d08d64e94 100644 --- a/backend/src/test/java/corea/fixture/RoomFixture.java +++ b/backend/src/test/java/corea/fixture/RoomFixture.java @@ -69,6 +69,24 @@ public static Room ROOM_DOMAIN(Member member, LocalDateTime recruitmentDeadline, ); } + public static Room ROOM_DOMAIN_WITH_CLASSIFICATION(Member member, LocalDateTime recruitmentDeadline, RoomClassification classification) { + return new Room( + "자바 레이싱 카 - MVC", + "MVC 패턴을 아시나요?", + 2, + "https://github.com/example/java-racingcar", + "https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=13301655&filePath=L2Rpc2sxL25ld2RhdGEvMjAyMS8yMS9DTFMxMDAwNC8xMzMwMTY1NV9XUlRfMjFfQ0xTMTAwMDRfMjAyMTEyMTNfMQ==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10004", + List.of("TDD, 클린코드,자바"), + 17, + 30, + member, + recruitmentDeadline, + LocalDateTime.now().plusDays(14), + classification, + RoomStatus.OPEN + ); + } + public static Room ROOM_DOMAIN(Long id, Member member) { return new Room( id, diff --git a/backend/src/test/java/corea/room/acceptance/RoomAcceptanceTest.java b/backend/src/test/java/corea/room/acceptance/RoomAcceptanceTest.java index d8e01092a..9973fb9a3 100644 --- a/backend/src/test/java/corea/room/acceptance/RoomAcceptanceTest.java +++ b/backend/src/test/java/corea/room/acceptance/RoomAcceptanceTest.java @@ -19,6 +19,7 @@ import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -133,6 +134,48 @@ void participatedRoomsWithLogin() { }); } + @Test + @DisplayName("참여 중인 방을 종료된 방도 포함해서 보여줄 수 있다.") + void participatedRooms_IncludeClosed() { + String accessToken = tokenService.createAccessToken(memberRepository.findByUsername("jcoding-play").get()); + + RoomResponses response = RestAssured.given().log().all() + .auth().oauth2(accessToken) + .when().get("/rooms/participated?includeClosed=true") + .then().log().all() + .statusCode(200) + .extract().as(RoomResponses.class); + + List rooms = response.rooms(); + + List managers = rooms.stream() + .map(RoomResponse::manager) + .toList(); + + assertThat(managers).containsExactlyInAnyOrder("조경찬", "강다빈", "이상엽", "최진실"); + } + + @Test + @DisplayName("참여 중인 방을 종료된 방도 제외해서 보여줄 수 있다.") + void participatedRooms_ExcludeClosed() { + String accessToken = tokenService.createAccessToken(memberRepository.findByUsername("jcoding-play").get()); + + RoomResponses response = RestAssured.given().log().all() + .auth().oauth2(accessToken) + .when().get("/rooms/participated?includeClosed=false") + .then().log().all() + .statusCode(200) + .extract().as(RoomResponses.class); + + List rooms = response.rooms(); + + List managers = rooms.stream() + .map(RoomResponse::manager) + .toList(); + + assertThat(managers).containsExactlyInAnyOrder("강다빈", "이상엽", "최진실"); + } + @Test @DisplayName("로그인하지 않은 사용자가 분야별로 현재 모집 중인 방들을 조회할 수 있다.") void openedRoomsWithoutLogin() { @@ -169,10 +212,11 @@ void openedRoomsWithLogin() { List rooms = response.rooms(); assertSoftly(softly -> { - softly.assertThat(rooms).hasSize(3); - softly.assertThat(rooms.get(0).manager()).isEqualTo("박민아"); - softly.assertThat(rooms.get(1).manager()).isEqualTo("포비"); + softly.assertThat(rooms).hasSize(4); + softly.assertThat(rooms.get(0).manager()).isEqualTo("조경찬"); + softly.assertThat(rooms.get(1).manager()).isEqualTo("박민아"); softly.assertThat(rooms.get(2).manager()).isEqualTo("포비"); + softly.assertThat(rooms.get(3).manager()).isEqualTo("포비"); }); } diff --git a/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java b/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java index ed6000c26..3a4ddd856 100644 --- a/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java +++ b/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java @@ -5,6 +5,8 @@ import corea.fixture.RoomFixture; import corea.member.domain.Member; import corea.member.repository.MemberRepository; +import corea.participation.domain.Participation; +import corea.participation.repository.ParticipationRepository; import corea.room.domain.Room; import corea.room.domain.RoomClassification; import corea.room.domain.RoomStatus; @@ -28,31 +30,47 @@ class RoomRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private ParticipationRepository participationRepository; + @Test - @DisplayName("자신이 참여하지 않고, 계속 모집 중인 방들을 모집 마감일이 임박한 순으로 조회할 수 있다.") - void findAllByMemberAndClassificationAndStatus() { + @DisplayName("선택한 분야와 일치하는 방들을 조회할 수 있다.") + void findAllByClassificationAndStatusOrderByRecruitmentDeadline() { Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); + roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLASSIFICATION(pororo, LocalDateTime.now().plusDays(2), RoomClassification.ANDROID)); + roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLASSIFICATION(joyson, LocalDateTime.now().plusDays(3), RoomClassification.BACKEND)); + + Page roomPage = roomRepository.findAllByClassificationAndStatusOrderByRecruitmentDeadline(RoomClassification.BACKEND, RoomStatus.OPEN, PageRequest.of(0, 8)); + + List managerNames = getManagerNames(roomPage.getContent()); + assertThat(managerNames).containsExactly("이영수"); + } + + @Test + @DisplayName("모집 중인 방들을 조회할 때 자신이 참여한 방도 포함하여 조회한다.") + void findAllByMemberAndStatus_participated() { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); roomRepository.save(RoomFixture.ROOM_DOMAIN(joyson, LocalDateTime.now().plusDays(3))); - Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); - Page roomPage = roomRepository.findAllByMemberAndClassificationAndStatus(movin.getId(), RoomClassification.BACKEND, RoomStatus.OPEN, PageRequest.of(0, 8)); + participationRepository.save(new Participation(pororoRoom, pororo)); + Page roomPage = roomRepository.findAllByStatusOrderByRecruitmentDeadline(RoomStatus.OPEN, PageRequest.of(0, 8)); List managerNames = getManagerNames(roomPage.getContent()); assertThat(managerNames).containsExactly("조경찬", "이영수"); } @Test - @DisplayName("분야와 상관 없이 자신이 참여하지 않고, 계속 모집 중인 방들을 모집 마감일이 임박한 순으로 조회할 수 있다.") - void findAllByMemberAndStatus() { + @DisplayName("모집 중인 방들을 모집 마감일이 임박한 순으로 조회할 수 있다.") + void findAllByMemberAndStatus_notParticipated() { Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); roomRepository.save(RoomFixture.ROOM_DOMAIN(joyson, LocalDateTime.now().plusDays(3))); - Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); - Page roomPage = roomRepository.findAllByMemberAndStatus(movin.getId(), RoomStatus.OPEN, PageRequest.of(0, 8)); + Page roomPage = roomRepository.findAllByStatusOrderByRecruitmentDeadline(RoomStatus.OPEN, PageRequest.of(0, 8)); List managerNames = getManagerNames(roomPage.getContent()); assertThat(managerNames).containsExactly("조경찬", "이영수"); diff --git a/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java b/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java index 52aeed75c..cdd091cb4 100644 --- a/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java @@ -5,12 +5,17 @@ import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; import corea.member.domain.Member; +import corea.member.domain.MemberRole; import corea.member.repository.MemberRepository; +import corea.participation.domain.Participation; +import corea.participation.repository.ParticipationRepository; +import corea.room.domain.Room; import corea.room.domain.RoomStatus; import corea.room.dto.RoomResponse; import corea.room.dto.RoomResponses; import corea.room.repository.RoomRepository; 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 org.junit.jupiter.params.provider.EnumSource; @@ -24,29 +29,33 @@ @ServiceTest public class RoomInquiryServiceTest { - @Autowired - private MemberRepository memberRepository; - @Autowired private RoomInquiryService roomInquiryService; @Autowired private RoomRepository roomRepository; + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ParticipationRepository participationRepository; + @ParameterizedTest @EnumSource(RoomStatus.class) - @DisplayName("로그인한 사용자가 자신이 참여하지 않은 방을 상태별로 마감일 임박순으로 조회할 수 있다.") + @DisplayName("로그인한 사용자가 방을 상태별로 마감일 임박순으로 조회할 수 있다.") void findRoomsWithRoomStatus_login_member(RoomStatus roomStatus) { Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2), roomStatus)); - roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); + participationRepository.save(new Participation(ashRoom, ash)); RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(pororo.getId(), 0, "all", roomStatus); List managerNames = getManagerNames(response); - assertThat(managerNames).containsExactly("박민아"); + assertThat(managerNames).containsExactly("조경찬", "박민아"); } @ParameterizedTest @@ -67,6 +76,79 @@ void findRoomsWithRoomStatus_non_login_member(RoomStatus roomStatus) { assertThat(managerNames).containsExactly("조경찬", "박민아"); } + private List getManagerNames(RoomResponses response) { + return response.rooms() + .stream() + .map(RoomResponse::manager) + .toList(); + } + + @Test + @DisplayName("현재 로그인한 멤버가 참여 중인 방을 리뷰 마감일이 임박한 순으로 볼 수 있다.") + void findParticipatedRooms() { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3))); + + Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + Long joysonId = joyson.getId(); + participationRepository.save(new Participation(pororoRoom, joyson, MemberRole.BOTH, pororoRoom.getMatchingSize())); + participationRepository.save(new Participation(ashRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + + RoomResponses response = roomInquiryService.findParticipatedRooms(joysonId, false); + List managerNames = getManagerNames(response); + + assertThat(managerNames).containsExactly("조경찬", "박민아"); + } + + @Test + @DisplayName("현재 로그인한 멤버가 참여 중인 방을 볼 때, 종료된 방을 포함하지 않을 수 있다.") + void findParticipatedRoomsWithoutClosed() { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); + + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3))); + Room movinRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(movin)); + + Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + Long joysonId = joyson.getId(); + participationRepository.save(new Participation(pororoRoom, joyson, MemberRole.BOTH, pororoRoom.getMatchingSize())); + participationRepository.save(new Participation(ashRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + participationRepository.save(new Participation(movinRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + + RoomResponses response = roomInquiryService.findParticipatedRooms(joysonId, false); + List managerNames = getManagerNames(response); + + assertThat(managerNames).containsExactly("조경찬", "박민아"); + } + + @Test + @DisplayName("현재 로그인한 멤버가 참여 중인 방을 볼 때, 종료된 방을 포함할 수 있다.") + void findParticipatedRoomsWithClosed() { + Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); + Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); + + Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); + Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3))); + Room movinRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(movin)); + + Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + Long joysonId = joyson.getId(); + participationRepository.save(new Participation(pororoRoom, joyson, MemberRole.BOTH, pororoRoom.getMatchingSize())); + participationRepository.save(new Participation(ashRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + participationRepository.save(new Participation(movinRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + + RoomResponses response = roomInquiryService.findParticipatedRooms(joysonId, true); + List managerNames = getManagerNames(response); + + assertThat(managerNames).containsExactly("조경찬", "박민아", "김현중"); + } + @ParameterizedTest @CsvSource(value = {"0, false", "1, true"}) @DisplayName("방을 조회할 때 전달받은 페이지가 마지막 페이지인지 판별할 수 있다.") @@ -81,11 +163,4 @@ void isLastPage(int pageNumber, boolean expected) { assertThat(response.isLastPage()).isEqualTo(expected); } - - private List getManagerNames(RoomResponses response) { - return response.rooms() - .stream() - .map(RoomResponse::manager) - .toList(); - } } diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index 761f3c74d..73b33ad92 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -19,19 +19,15 @@ import corea.room.dto.RoomCreateRequest; import corea.room.dto.RoomParticipantResponses; import corea.room.dto.RoomResponse; -import corea.room.dto.RoomResponses; +import corea.room.dto.RoomUpdateRequest; import corea.room.repository.RoomRepository; import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -56,211 +52,161 @@ class RoomServiceTest { @Autowired private ParticipationRepository participationRepository; - @Autowired - private FailedMatchingRepository failedMatchingRepository; - - @Test - @DisplayName("방을 생성할 수 있다.") - void create() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); - - assertThat(roomRepository.findAll()).hasSize(1); - } - - @Test - @DisplayName("방의 매니저가 아니면 수정 시, 예외를 발생합니다.") - void throw_exception_when_update_with_not_manager() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); - assertThatThrownBy(() -> roomService.update(-1, RoomFixture.ROOM_UPDATE_REQUEST(response.id()))) - .isInstanceOf(CoreaException.class); - } - - @Test - @DisplayName("존재하지 않는 방이면, 예외를 발생합니다.") - void throw_exception_when_update_with_not_exist_room() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); - assertThatThrownBy(() -> roomService.update(manager.getId(), RoomFixture.ROOM_UPDATE_REQUEST(-1))) - .isInstanceOf(CoreaException.class); - } - - @Disabled - @Test - @DisplayName("방을 생성할 때 모집 마감 시간은 현재 시간보다 1시간 이후가 아니라면 예외가 발생한다.") - void invalidRecruitmentDeadline() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST_WITH_RECRUITMENT_DEADLINE(LocalDateTime.now() - .plusMinutes(59)); - - assertThatThrownBy(() -> roomService.create(manager.getId(), request)) - .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) - .extracting(CoreaException::getExceptionType) - .isEqualTo(ExceptionType.INVALID_RECRUITMENT_DEADLINE); - } - - @Disabled - @Test - @DisplayName("방을 생성할 때 리뷰 마감 시간은 모집 마감 시간보다 1일 이후가 아니라면 예외가 발생한다.") - void invalidReviewDeadline() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(LocalDateTime.now() - .plusHours(2), LocalDateTime.now() - .plusDays(1)); - - assertThatThrownBy(() -> roomService.create(manager.getId(), request)) - .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) - .extracting(CoreaException::getExceptionType) - .isEqualTo(ExceptionType.INVALID_REVIEW_DEADLINE); - } - - @Test - @DisplayName("방을 만든 사람이 방을 조회할 때 자신의 참여 상태가 방장이란 것을 알 수 있다.") - void findOne_manager() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(); - RoomResponse response = roomService.create(manager.getId(), request); - - response = roomService.findOne(response.id(), manager.getId()); - - assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.MANAGER); - } - - @Test - @DisplayName("방을 조회할 때 자신의 참여 상태를 알 수 있다.") - void findOne_participated() { - 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())); - - RoomResponse response = roomService.findOne(room.getId(), member.getId()); - - assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.PARTICIPATED); + @Nested + @DisplayName("방을 생성, 수정 및 삭제할 수 있다.") + class RoomWriter { + + private Member manager; + + @BeforeEach + void setUp() { + manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + } + + @Test + @DisplayName("방을 생성할 수 있다.") + void create() { + roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + + assertThat(roomRepository.findAll()).hasSize(1); + } + + @Disabled + @Test + @DisplayName("방을 생성할 때 모집 마감 시간은 현재 시간보다 1시간 이후가 아니라면 예외가 발생한다.") + void invalidRecruitmentDeadline() { + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST_WITH_RECRUITMENT_DEADLINE(LocalDateTime.now().plusMinutes(59)); + + assertThatThrownBy(() -> roomService.create(manager.getId(), request)) + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.INVALID_RECRUITMENT_DEADLINE); + } + + @Disabled + @Test + @DisplayName("방을 생성할 때 리뷰 마감 시간은 모집 마감 시간보다 1일 이후가 아니라면 예외가 발생한다.") + void invalidReviewDeadline() { + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(LocalDateTime.now().plusHours(2), LocalDateTime.now().plusDays(1)); + + assertThatThrownBy(() -> roomService.create(manager.getId(), request)) + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.INVALID_REVIEW_DEADLINE); + } + + @Test + @DisplayName("방을 삭제할 수 있다.") + void delete() { + RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + + long roomId = response.id(); + roomService.delete(roomId, manager.getId()); + + assertThat(roomRepository.findById(roomId)).isEmpty(); + } + + @Test + @DisplayName("방을 생성한 유저가 아닌 사람이 방을 삭제하려고 하면 예외가 발생한다.") + void invalidDelete() { + RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + + Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); + + assertThatThrownBy(() -> roomService.delete(response.id(), member.getId())) + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.ROOM_DELETION_AUTHORIZATION_ERROR); + } + + @Test + @DisplayName("방의 매니저가 아니면 수정 시, 예외를 발생합니다.") + void throw_exception_when_update_with_not_manager() { + RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + + assertThatThrownBy(() -> roomService.update(-1, RoomFixture.ROOM_UPDATE_REQUEST(response.id()))) + .isInstanceOf(CoreaException.class); + } + + @Test + @DisplayName("존재하지 않는 방이면, 예외를 발생합니다.") + void throw_exception_when_update_with_not_exist_room() { + roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); + + assertThatThrownBy(() -> roomService.update(manager.getId(), RoomFixture.ROOM_UPDATE_REQUEST(-1))) + .isInstanceOf(CoreaException.class); + } } - @Test - @DisplayName("방을 조회할 때 자신의 참여 상태를 알 수 있다.") - void findOne_not_participated() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); - - Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); - RoomResponse response = roomService.findOne(room.getId(), member.getId()); + @Nested + @DisplayName("방을 조회할 수 있다.") + class RoomReader { - assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.NOT_PARTICIPATED); - } + private Member manager; + private Member member; + private Room room; - @Test - @DisplayName("매칭을 실패한 방을 조회할 때 실패한 원인에 대해 알 수 있다.") - void findOne_with_matching_fail() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); + @BeforeEach + void setUp() { + manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + member = memberRepository.save(MemberFixture.MEMBER_PORORO()); + room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); + } - Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); - participationRepository.save(new Participation(room, member, MemberRole.BOTH, room.getMatchingSize())); + @Test + @DisplayName("조회하는 방을 만든 사람은 방장이다.") + void manager() { + RoomResponse response = roomService.create(manager.getId(), RoomFixture.ROOM_CREATE_REQUEST()); - failedMatchingRepository.save(new FailedMatching(room.getId(), ExceptionType.PARTICIPANT_SIZE_LACK)); - RoomResponse response = roomService.findOne(room.getId(), member.getId()); + assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.MANAGER); + } - assertThat(response.message()).isEqualTo("방의 최소 참여 인원보다 참가자가 부족하여 매칭이 진행되지 않았습니다."); - } + @Test + @DisplayName("조회하는 방에 참여했다면 참여자이다.") + void participated() { + participationRepository.save(new Participation(room, member, MemberRole.BOTH, room.getMatchingSize())); - @Test - @DisplayName("현재 로그인한 멤버가 참여 중인 방을 리뷰 마감일이 임박한 순으로 볼 수 있다.") - void findParticipatedRooms() { - Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); - - Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now() - .plusDays(2))); - Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now() - .plusDays(3))); + RoomResponse response = roomService.findOne(room.getId(), member.getId()); - Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - Long joysonId = joyson.getId(); - participationRepository.save(new Participation(pororoRoom, joyson, MemberRole.BOTH, pororoRoom.getMatchingSize())); - participationRepository.save(new Participation(ashRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); + assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.PARTICIPATED); + } - RoomResponses response = roomService.findParticipatedRooms(joysonId); - List managerNames = getManagerNames(response); + @Test + @DisplayName("조회하는 방에 참여하지 않았다면 참여자가 아니다.") + void not_participated() { + RoomResponse response = roomService.findOne(room.getId(), member.getId()); - assertThat(managerNames).containsExactly("조경찬", "박민아"); + assertThat(response.participationStatus()).isEqualTo(ParticipationStatus.NOT_PARTICIPATED); + } } - @Test - @DisplayName("현재 로그인한 멤버가 참여 중인 방을 볼 때, 종료된 방은 포함되지 않는다.") - void findNonClosedParticipatedRooms() { - Member pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); - Member ash = memberRepository.save(MemberFixture.MEMBER_ASH()); - Member movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); - - Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now() - .plusDays(2))); - Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now() - .plusDays(3))); - Room movinRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(movin)); - - Member joyson = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); - Long joysonId = joyson.getId(); - participationRepository.save(new Participation(pororoRoom, joyson, MemberRole.BOTH, pororoRoom.getMatchingSize())); - participationRepository.save(new Participation(ashRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); - participationRepository.save(new Participation(movinRoom, joyson, MemberRole.BOTH, ashRoom.getMatchingSize())); - - RoomResponses response = roomService.findParticipatedRooms(joysonId); - List managerNames = getManagerNames(response); - - assertThat(managerNames).containsExactly("조경찬", "박민아"); - } + @Nested + @DisplayName("방 매칭이 실패 했을 경우 실패한 원인에 대해 알 수 있다.") + class MatchingFailedRoom { - @Test - @DisplayName("방을 생성한 방장의 참여 상태는 MANAGER다.") - void create_participationStatus_manager() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(); - RoomResponse response = roomService.create(manager.getId(), request); + @Autowired + private FailedMatchingRepository failedMatchingRepository; - Optional participation = participationRepository.findByRoomIdAndMemberId(response.id(), manager.getId()); + private Member manager; + private Room room; - assertAll( - () -> assertThat(response.manager()).isEqualTo(manager.getName()), - () -> assertThat(participation.isPresent()).isTrue(), - () -> assertThat(participation.get() - .getStatus()).isEqualTo(ParticipationStatus.MANAGER) - ); - } + @BeforeEach + void setUp() { + manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); + } - @Test - @DisplayName("방을 삭제할 수 있다.") - void delete() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + @Test + @DisplayName("방 참여자의 수가 최소 매칭 인원보다 작다면 매칭이 진행되지 않았다면 메세지를 통해 원인을 파악할 수 있다.") + void participant_size_lack() { + Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); + participationRepository.save(new Participation(room, member, MemberRole.BOTH, room.getMatchingSize())); - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(); - RoomResponse response = roomService.create(manager.getId(), request); + failedMatchingRepository.save(new FailedMatching(room.getId(), ExceptionType.PARTICIPANT_SIZE_LACK)); + RoomResponse response = roomService.findOne(room.getId(), member.getId()); - long roomId = response.id(); - roomService.delete(roomId, manager.getId()); - - assertThat(roomRepository.findById(roomId)).isEmpty(); - } - - @Test - @DisplayName("방을 생성한 유저가 아닌 사람이 방을 삭제하려고 하면 예외가 발생한다.") - void invalidDelete() { - Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); - Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); - - Member member = memberRepository.save(MemberFixture.MEMBER_PORORO()); - - assertThatThrownBy(() -> roomService.delete(room.getId(), member.getId())) - .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) - .extracting(CoreaException::getExceptionType) - .isEqualTo(ExceptionType.ROOM_DELETION_AUTHORIZATION_ERROR); + assertThat(response.message()).isEqualTo("방의 최소 참여 인원보다 참가자가 부족하여 매칭이 진행되지 않았습니다."); + } } @Test @@ -273,13 +219,9 @@ void findParticipants() { List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); participationRepository.save(new Participation(room, manager)); - participationRepository.saveAll(members.stream() - .map(member -> new Participation(room, member, MemberRole.BOTH, 2)) - .toList()); + participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); - matchResultRepository.saveAll(members.stream() - .map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)) - .toList()); + matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); RoomParticipantResponses participants = roomService.findParticipants(room.getId(), manager.getId()); @@ -302,13 +244,9 @@ void findParticipants_withNoPullRequestParticipants() { List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); - participationRepository.saveAll(members.stream() - .map(member -> new Participation(room, member, MemberRole.BOTH, 2)) - .toList()); + participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); - matchResultRepository.saveAll(members.stream() - .map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)) - .toList()); + matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); RoomParticipantResponses participants = assertDoesNotThrow(() -> roomService.findParticipants(room.getId(), manager.getId())); @@ -318,11 +256,4 @@ void findParticipants_withNoPullRequestParticipants() { () -> assertThat(participants.size()).isEqualTo(6) ); } - - private List getManagerNames(RoomResponses response) { - return response.rooms() - .stream() - .map(RoomResponse::manager) - .toList(); - } } From 48256cdcfc6b6fe0d54f30f7ce7b4ea941e426c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:36:59 +0900 Subject: [PATCH 11/31] =?UTF-8?q?[BE]=20=EB=A1=9C=EA=B7=B8=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=9E=91=EC=84=B1=20=EC=A0=9C=EA=B1=B0,=20?= =?UTF-8?q?=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=82=B4=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80(#608)=20(#6?= =?UTF-8?q?10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: additivity false 로 변경 * refactor: 참가,취소 로직 분리 * chore: 로그 레벨 변경 * feat: 로그 추가 * refactor: 피드백 반영 --------- Co-authored-by: youngsu5582 <98307410+youngsu5582@users.noreply.github.com> --- .../resolver/LoginMemberArgumentResolver.java | 1 - .../java/corea/auth/service/LoginService.java | 13 ++++- .../service/DevelopFeedbackService.java | 4 +- .../service/SocialFeedbackService.java | 4 +- .../matching/domain/ParticipationFilter.java | 3 + .../java/corea/member/domain/MemberRole.java | 8 +++ .../participation/domain/Participation.java | 13 ++--- .../service/ParticipationService.java | 15 ++--- .../service/ParticipationWriter.java | 57 +++++++++++++++++++ .../java/corea/room/service/RoomService.java | 8 ++- backend/src/main/resources/logback-spring.xml | 4 +- .../domain/ParticipationFilterTest.java | 12 ++-- .../service/ParticipationServiceTest.java | 4 +- .../room/repository/RoomRepositoryTest.java | 4 +- .../room/service/RoomInquiryServiceTest.java | 3 +- .../corea/room/service/RoomServiceTest.java | 6 +- 16 files changed, 117 insertions(+), 42 deletions(-) create mode 100644 backend/src/main/java/corea/participation/service/ParticipationWriter.java diff --git a/backend/src/main/java/corea/auth/resolver/LoginMemberArgumentResolver.java b/backend/src/main/java/corea/auth/resolver/LoginMemberArgumentResolver.java index ab89710ee..0bf445a88 100644 --- a/backend/src/main/java/corea/auth/resolver/LoginMemberArgumentResolver.java +++ b/backend/src/main/java/corea/auth/resolver/LoginMemberArgumentResolver.java @@ -45,7 +45,6 @@ public AuthInfo resolveArgument(MethodParameter parameter, ModelAndViewContainer if (accessToken.equals(ANONYMOUS)) { throw new CoreaException(ExceptionType.AUTHORIZATION_ERROR); } - log.info("로그인 시도[토큰={}]", accessToken); long memberId = tokenService.findMemberIdByToken(accessToken); Member member = memberRepository.findById(memberId) diff --git a/backend/src/main/java/corea/auth/service/LoginService.java b/backend/src/main/java/corea/auth/service/LoginService.java index 300feea06..260765821 100644 --- a/backend/src/main/java/corea/auth/service/LoginService.java +++ b/backend/src/main/java/corea/auth/service/LoginService.java @@ -8,12 +8,14 @@ import corea.member.domain.Member; import corea.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import static corea.exception.ExceptionType.INVALID_TOKEN; import static corea.exception.ExceptionType.TOKEN_EXPIRED; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -35,7 +37,13 @@ public TokenInfo login(GithubUserInfo userInfo) { } private Member register(GithubUserInfo userInfo) { - return memberRepository.save(new Member(userInfo.login(), userInfo.avatarUrl(), userInfo.name(), userInfo.email(), true, userInfo.id())); + Member member = memberRepository.save(new Member(userInfo.login(), userInfo.avatarUrl(), userInfo.name(), userInfo.email(), true, userInfo.id())); + logCreateMembers(member); + return member; + } + + private void logCreateMembers(Member member) { + log.info("멤버를 생성했습니다. 멤버 id={}, 멤버 이름={},깃허브 id={}, 닉네임={}", member.getId(), member.getName(), member.getGithubUserId(), member.getUsername()); } private String extendAuthorization(Member member) { @@ -56,7 +64,8 @@ public String refresh(String refreshToken) { .orElseThrow(() -> new CoreaException(INVALID_TOKEN)); return tokenService.createAccessToken(info.getMember()); } catch (CoreaException e) { - if (e.getExceptionType().equals(TOKEN_EXPIRED)) { + if (e.getExceptionType() + .equals(TOKEN_EXPIRED)) { logoutService.logoutByExpiredRefreshToken(refreshToken); } throw e; diff --git a/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java b/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java index 1f59d30ea..fa449c5df 100644 --- a/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java +++ b/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java @@ -27,7 +27,7 @@ public class DevelopFeedbackService { @Transactional public DevelopFeedbackResponse create(long roomId, long deliverId, DevelopFeedbackRequest request) { validateAlreadyExist(roomId, deliverId, request.receiverId()); - log.debug("개발 피드백 작성[작성자({}), 요청값({})", deliverId, request); + log.info("개발 피드백 작성[작성자({}), 요청값({})", deliverId, request); MatchResult matchResult = matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, deliverId, request.receiverId()) .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); @@ -50,7 +50,7 @@ private DevelopFeedback saveDevelopFeedback(long roomId, DevelopFeedbackRequest @Transactional public DevelopFeedbackResponse update(long feedbackId, long deliverId, DevelopFeedbackRequest request) { - log.debug("개발 피드백 업데이트[작성자({}), 피드백 ID({}), 요청값({})", deliverId, feedbackId, request); + log.info("개발 피드백 업데이트[작성자({}), 피드백 ID({}), 요청값({})", deliverId, feedbackId, request); DevelopFeedback feedback = developFeedbackRepository.findById(feedbackId) .orElseThrow(() -> new CoreaException(ExceptionType.FEEDBACK_NOT_FOUND)); diff --git a/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java b/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java index 6b536e4c4..7c3bc66c1 100644 --- a/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java +++ b/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java @@ -27,7 +27,7 @@ public class SocialFeedbackService { @Transactional public SocialFeedbackResponse create(long roomId, long deliverId, SocialFeedbackRequest request) { validateAlreadyExist(roomId, deliverId, request.receiverId()); - log.debug("소설 피드백 작성[작성자({}), 요청값({})", deliverId, request); + log.info("소설 피드백 작성[작성자({}), 요청값({})", deliverId, request); MatchResult matchResult = matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, request.receiverId(), deliverId) .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); @@ -50,7 +50,7 @@ private SocialFeedback saveSocialFeedback(long roomId, SocialFeedbackRequest req @Transactional public SocialFeedbackResponse update(long feedbackId, long deliverId, SocialFeedbackRequest request) { - log.debug("소설 피드백 업데이트[작성자({}), 피드백 ID({}), 요청값({})", deliverId, feedbackId, request); + log.info("소설 피드백 업데이트[작성자({}), 피드백 ID({}), 요청값({})", deliverId, feedbackId, request); SocialFeedback feedback = socialFeedbackRepository.findById(feedbackId) .orElseThrow(() -> new CoreaException(ExceptionType.FEEDBACK_NOT_FOUND)); diff --git a/backend/src/main/java/corea/matching/domain/ParticipationFilter.java b/backend/src/main/java/corea/matching/domain/ParticipationFilter.java index 40b738ab1..fe8f40a73 100644 --- a/backend/src/main/java/corea/matching/domain/ParticipationFilter.java +++ b/backend/src/main/java/corea/matching/domain/ParticipationFilter.java @@ -3,10 +3,12 @@ import corea.exception.CoreaException; import corea.exception.ExceptionType; import corea.participation.domain.Participation; +import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; +@Slf4j public class ParticipationFilter { private final List participations; @@ -51,6 +53,7 @@ private List findPRSubmittedParticipation(PullRequestInfo pullReq private void invalidateIfNotSubmitPR(PullRequestInfo pullRequestInfo, Participation participation) { if (!hasSubmittedPR(pullRequestInfo, participation)) { + log.warn("매칭에 실패 했습니다. 방 id={},사용자 id={},사용자 깃허브 닉네임={}",participation.getRoomsId(),participation.getMembersId(),participation.getMember().getUsername()); participation.invalidate(); } } diff --git a/backend/src/main/java/corea/member/domain/MemberRole.java b/backend/src/main/java/corea/member/domain/MemberRole.java index 0af4599c9..f4a38ae57 100644 --- a/backend/src/main/java/corea/member/domain/MemberRole.java +++ b/backend/src/main/java/corea/member/domain/MemberRole.java @@ -2,6 +2,7 @@ import corea.exception.CoreaException; import corea.exception.ExceptionType; +import corea.participation.domain.ParticipationStatus; public enum MemberRole { @@ -22,4 +23,11 @@ public static MemberRole from(String role) { public boolean isReviewer() { return this == REVIEWER; } + + public ParticipationStatus getParticipationStatus() { + return switch (this) { + case REVIEWER,REVIEWEE,BOTH -> ParticipationStatus.PARTICIPATED; + case NONE -> ParticipationStatus.NOT_PARTICIPATED; + }; + } } diff --git a/backend/src/main/java/corea/participation/domain/Participation.java b/backend/src/main/java/corea/participation/domain/Participation.java index 29e9e3cba..680027c6d 100644 --- a/backend/src/main/java/corea/participation/domain/Participation.java +++ b/backend/src/main/java/corea/participation/domain/Participation.java @@ -40,14 +40,12 @@ public class Participation extends BaseTimeEntity { private int matchingSize; - public Participation(Room room, Member member, MemberRole role, int matchingSize) { - this(null, room, member, role, ParticipationStatus.PARTICIPATED, matchingSize); - debug(room.getId(), member.getId()); + public Participation(Room room, Member member, MemberRole memberRole, ParticipationStatus status, int matchingSize) { + this(null, room, member, memberRole, status, matchingSize); } - public Participation(Room room, Member member) { - this(null, room, member, MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize()); - debug(room.getId(), member.getId()); + public Participation(Room room, Member member, MemberRole role, int matchingSize) { + this(null, room, member, role, ParticipationStatus.PARTICIPATED, matchingSize); } public boolean isNotMatchingMemberId(long memberId) { @@ -94,7 +92,4 @@ public String getMemberGithubId() { return member.getGithubUserId(); } - private static void debug(long roomId, long memberId) { - log.debug("참가자 생성[방 ID={}, 멤버 ID={}", roomId, memberId); - } } diff --git a/backend/src/main/java/corea/participation/service/ParticipationService.java b/backend/src/main/java/corea/participation/service/ParticipationService.java index fc84b0ee6..c3cd8e4dc 100644 --- a/backend/src/main/java/corea/participation/service/ParticipationService.java +++ b/backend/src/main/java/corea/participation/service/ParticipationService.java @@ -21,6 +21,7 @@ public class ParticipationService { private final ParticipationRepository participationRepository; + private final ParticipationWriter participationWriter; private final RoomRepository roomRepository; private final MemberRepository memberRepository; @@ -34,23 +35,15 @@ private Participation saveParticipation(ParticipationRequest request) { Member member = memberRepository.findById(request.memberId()) .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); MemberRole memberRole = MemberRole.from(request.role()); + Room room = getRoom(request.roomId()); - Participation participation = new Participation(getRoom(request.roomId()), member, memberRole, request.matchingSize()); - participation.participate(); - return participationRepository.save(participation); + return participationWriter.create(room, member, memberRole, request.matchingSize()); } @Transactional public void cancel(long roomId, long memberId) { validateMemberExist(memberId); - deleteParticipation(roomId, memberId); - } - - private void deleteParticipation(long roomId, long memberId) { - Participation participation = participationRepository.findByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new CoreaException(ExceptionType.NOT_ALREADY_APPLY)); - participation.cancel(); - participationRepository.delete(participation); + participationWriter.delete(roomId, memberId); } private void validateIdExist(long roomId, long memberId) { diff --git a/backend/src/main/java/corea/participation/service/ParticipationWriter.java b/backend/src/main/java/corea/participation/service/ParticipationWriter.java new file mode 100644 index 000000000..46ae8a793 --- /dev/null +++ b/backend/src/main/java/corea/participation/service/ParticipationWriter.java @@ -0,0 +1,57 @@ +package corea.participation.service; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.member.domain.Member; +import corea.member.domain.MemberRole; +import corea.participation.domain.Participation; +import corea.participation.domain.ParticipationStatus; +import corea.participation.repository.ParticipationRepository; +import corea.room.domain.Room; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional +public class ParticipationWriter { + + private final ParticipationRepository participationRepository; + + public Participation create(Room room, Member member, MemberRole memberRole, ParticipationStatus participationStatus) { + + return create(room, member, memberRole, participationStatus, room.getMatchingSize()); + } + + public Participation create(Room room, Member member, MemberRole memberRole, int matchingSize) { + return create(room, member, memberRole, memberRole.getParticipationStatus(), matchingSize); + } + + private Participation create(Room room, Member member, MemberRole memberRole, ParticipationStatus participationStatus, int matchingSize) { + Participation participation = participationRepository.save(new Participation(room, member, memberRole, participationStatus, matchingSize)); + participation.participate(); + logCreateParticipation(participation); + return participation; + } + + // TODO 객체 두개를 넣어서 삭제하는 방향으로 변경 해야합니다. + // 현재, 로직상 cancel 호출 시, 의도하지 않는 room 조회문 발생 + public void delete(long roomId, long memberId) { + Participation participation = participationRepository.findByRoomIdAndMemberId(roomId, memberId) + .orElseThrow(() -> new CoreaException(ExceptionType.NOT_ALREADY_APPLY)); + participation.cancel(); + logDeleteParticipation(participation); + participationRepository.delete(participation); + } + + private void logCreateParticipation(Participation participation) { + log.info("방에 참가했습니다. id={}, 방 id={}, 참가한 사용자 id={}, 역할={}, 원하는 매칭 인원={}", participation.getId(), participation.getRoomsId(), participation.getMembersId(), participation.getMemberRole(), participation.getMatchingSize()); + } + + private void logDeleteParticipation(Participation participation) { + log.info("참여를 취소했습니다. 방 id={}, 참가한 사용자 id={}", participation.getRoomsId(), participation.getMembersId()); + } +} diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index 44b07f643..afc599e43 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -10,6 +10,7 @@ import corea.participation.domain.Participation; import corea.participation.domain.ParticipationStatus; import corea.participation.repository.ParticipationRepository; +import corea.participation.service.ParticipationWriter; import corea.room.domain.Room; import corea.room.dto.*; import corea.room.repository.RoomRepository; @@ -39,7 +40,7 @@ public class RoomService { private final ParticipationRepository participationRepository; private final FailedMatchingRepository failedMatchingRepository; private final RoomAutomaticService roomAutomaticService; - + private final ParticipationWriter participationWriter; @Transactional public RoomResponse create(long memberId, RoomCreateRequest request) { @@ -48,7 +49,9 @@ public RoomResponse create(long memberId, RoomCreateRequest request) { Member manager = memberRepository.findById(memberId) .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); Room room = roomRepository.save(request.toEntity(manager)); - Participation participation = new Participation(room, manager); + log.info("방을 생성했습니다. 방 생성자 id={}, 요청한 사용자 id={}", room.getManagerId(), memberId); + + Participation participation = participationWriter.create(room, manager, MemberRole.REVIEWER, ParticipationStatus.MANAGER); participationRepository.save(participation); roomAutomaticService.createAutomatic(room); @@ -107,6 +110,7 @@ public void delete(long roomId, long memberId) { Room room = getRoom(roomId); validateDeletionAuthority(room, memberId); + log.info("방을 삭제했습니다. 방 id={}, 사용자 iD={}", roomId, memberId); roomRepository.delete(room); participationRepository.deleteAllByRoomId(roomId); roomAutomaticService.deleteAutomatic(room); diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 7f4ad391a..b3b508a76 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -27,7 +27,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/backend/src/test/java/corea/matching/domain/ParticipationFilterTest.java b/backend/src/test/java/corea/matching/domain/ParticipationFilterTest.java index dbd1ec0e4..f82ad27a4 100644 --- a/backend/src/test/java/corea/matching/domain/ParticipationFilterTest.java +++ b/backend/src/test/java/corea/matching/domain/ParticipationFilterTest.java @@ -9,6 +9,7 @@ import corea.member.domain.Member; import corea.member.domain.MemberRole; import corea.participation.domain.Participation; +import corea.participation.domain.ParticipationStatus; import corea.room.domain.Room; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.DisplayName; @@ -33,10 +34,11 @@ class ParticipationFilterTest { @Test @DisplayName("방 참여자의 수가 매칭 사이즈보다 작거나 같다면 예외가 발생한다.") void invalid() { + + Room movinRoom = RoomFixture.ROOM_DOMAIN(1L, movin); List participations = List.of( - new Participation(RoomFixture.ROOM_DOMAIN(1L, pororo), pororo), - new Participation(RoomFixture.ROOM_DOMAIN(1L, movin), movin) - ); + new Participation(movinRoom, pororo,MemberRole.REVIEWER, ParticipationStatus.MANAGER, movinRoom.getMatchingSize()), + new Participation(movinRoom, movin,MemberRole.REVIEWER, ParticipationStatus.MANAGER, movinRoom.getMatchingSize())); assertThatThrownBy(() -> new ParticipationFilter(participations, 2)) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) @@ -51,7 +53,7 @@ void filterPRSubmittedParticipation() { List participations = List.of( new Participation(room, pororo, MemberRole.BOTH, 2), new Participation(room, movin, MemberRole.BOTH, 2), - new Participation(room, joyson), + new Participation(room, joyson,MemberRole.REVIEWER, ParticipationStatus.MANAGER, 2), new Participation(room, choco, MemberRole.BOTH, 2) ); @@ -70,7 +72,7 @@ void filterPRSubmittedParticipation() { void validatePRSubmittedParticipationSize() { Room room = RoomFixture.ROOM_DOMAIN(1L, pororo); List participations = List.of( - new Participation(room, pororo), + new Participation(room, pororo,MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize()), new Participation(room, movin, MemberRole.BOTH, 2), new Participation(room, choco, MemberRole.BOTH, 2) ); diff --git a/backend/src/test/java/corea/participation/service/ParticipationServiceTest.java b/backend/src/test/java/corea/participation/service/ParticipationServiceTest.java index 5c5319e36..8eb75c6fa 100644 --- a/backend/src/test/java/corea/participation/service/ParticipationServiceTest.java +++ b/backend/src/test/java/corea/participation/service/ParticipationServiceTest.java @@ -5,8 +5,10 @@ import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; 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.dto.ParticipationRequest; import corea.participation.repository.ParticipationRepository; import corea.room.domain.Room; @@ -92,7 +94,7 @@ void cancel_participate() { Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); Member member = memberRepository.save(MEMBER_YOUNGSU()); Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); - participationRepository.save(new Participation(room, member)); + participationRepository.save(new Participation(room, member, MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize())); participationService.cancel(room.getId(), member.getId()); diff --git a/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java b/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java index 3a4ddd856..e9c7ba1be 100644 --- a/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java +++ b/backend/src/test/java/corea/room/repository/RoomRepositoryTest.java @@ -4,8 +4,10 @@ import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; 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; @@ -55,7 +57,7 @@ void findAllByMemberAndStatus_participated() { Room pororoRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2))); roomRepository.save(RoomFixture.ROOM_DOMAIN(joyson, LocalDateTime.now().plusDays(3))); - participationRepository.save(new Participation(pororoRoom, pororo)); + participationRepository.save(new Participation(pororoRoom, pororo, MemberRole.REVIEWER, ParticipationStatus.MANAGER,pororoRoom.getMatchingSize())); Page roomPage = roomRepository.findAllByStatusOrderByRecruitmentDeadline(RoomStatus.OPEN, PageRequest.of(0, 8)); List managerNames = getManagerNames(roomPage.getContent()); diff --git a/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java b/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java index cdd091cb4..1b7999ee9 100644 --- a/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomInquiryServiceTest.java @@ -8,6 +8,7 @@ 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.RoomStatus; @@ -50,7 +51,7 @@ void findRoomsWithRoomStatus_login_member(RoomStatus roomStatus) { roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusDays(2), roomStatus)); Room ashRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusDays(3), roomStatus)); - participationRepository.save(new Participation(ashRoom, ash)); + participationRepository.save(new Participation(ashRoom, ash,MemberRole.REVIEWER, ParticipationStatus.MANAGER, ashRoom.getMatchingSize())); RoomResponses response = roomInquiryService.findRoomsWithRoomStatus(pororo.getId(), 0, "all", roomStatus); List managerNames = getManagerNames(response); diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index 73b33ad92..b1f15bef1 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -214,11 +214,11 @@ void participant_size_lack() { void findParticipants() { Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); - participationRepository.save(new Participation(room, manager)); + participationRepository.save(new Participation(room, manager,MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize())); List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); - participationRepository.save(new Participation(room, manager)); + participationRepository.save(new Participation(room, manager,MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize())); participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); @@ -238,7 +238,7 @@ void findParticipants_withNoPullRequestParticipants() { Member pullRequestNotSubmittedMember = memberRepository.save(MemberFixture.MEMBER_ASH()); Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); - participationRepository.save(new Participation(room, manager)); + participationRepository.save(new Participation(room, manager,MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize())); Participation participation = participationRepository.save(new Participation(room, pullRequestNotSubmittedMember, MemberRole.BOTH, 2)); participation.invalidate(); From 2390af537695d560947f5a51f27647e6efbcee94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:34:27 +0900 Subject: [PATCH 12/31] =?UTF-8?q?[FE]=2024=EC=8B=9C=EA=B0=84=20=EC=9D=B4?= =?UTF-8?q?=ED=95=98=EB=A1=9C=20=EB=82=A8=EC=9C=BC=EB=A9=B4=20=EB=94=94?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=20=EB=A7=90=EA=B3=A0=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0(#598)=20(#609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 디데이가 1일 남은경우에도 24시간 미만 남은경우 남은 dday로 반환하도록 변경 * refactor: areDatesEqual 함수를 dateFormatter 유틸 함수로 이동 * fix: 중복된 함수 제거 * test: 데이터 포메팅 유틸 함수 테스트코드 추가 * test: 테스트 시 이미지 모듈은 mock string 값이 반환되도록 모킹 * test: 룸 카드의 남은 기간 정보 UI 렌더링 테스트 추가 * fix: 진행중인 방의 D-Day 가 잘못 표기되는 문제 해결 * test: 테스트 명세 오타 수정 * test: 방 상세정보의 모집/리뷰 마감까지 남은 시간 렌더링 테스트 추가 * test: jest 환경에서 시간대를 한국 시간대를 사용하도록 변경 * feat: SEO 최적화를 위해 html에 메타태그 추가 --------- Co-authored-by: Lee sang Yeop --- .github/workflows/frontend-ci.yml | 3 + frontend/jest.setup.js | 3 + frontend/public/index.html | 6 +- .../components/common/calendar/Calendar.tsx | 2 +- .../roomInfoCard/RoomInfoCard.test.tsx | 171 ++++++++++ .../roomInfoCard/RoomInfoCard.tsx | 7 +- .../shared/roomCard/RoomCard.test.tsx | 298 ++++++++++++++++++ .../components/shared/roomCard/RoomCard.tsx | 2 +- frontend/src/utils/areDatesEqual.ts | 9 - frontend/src/utils/dateFormatter.test.ts | 120 +++++++ frontend/src/utils/dateFormatter.ts | 27 +- 11 files changed, 619 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.test.tsx create mode 100644 frontend/src/components/shared/roomCard/RoomCard.test.tsx delete mode 100644 frontend/src/utils/areDatesEqual.ts create mode 100644 frontend/src/utils/dateFormatter.test.ts diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index a1a4d828d..cd618a2cd 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -29,6 +29,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set timezone to Asia/Seoul + run: sudo timedatectl set-timezone Asia/Seoul + # 노드 설치 - name: Install Nodejs uses: actions/setup-node@v4 diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js index bf40478e9..3f67b25ef 100644 --- a/frontend/jest.setup.js +++ b/frontend/jest.setup.js @@ -1,5 +1,8 @@ const { server } = require("@/mocks/server"); +jest.mock("@/assets", () => ""); +process.env.TZ = "Asia/Seoul"; + // 모든 테스트 전에 MSW 서버를 시작합니다. beforeAll(() => server.listen()); diff --git a/frontend/public/index.html b/frontend/public/index.html index e417bd044..dfdb3be80 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -8,16 +8,18 @@ name="description" content="주니어 개발자들이 서로 코드리뷰하고 피드백 받을 수 있는 플랫폼" /> - CoReA + CoReA(Code Review Area) - 코드리뷰 매칭 플랫폼 - + + + { + beforeAll(() => { + const mockDate = new Date("2024-10-02T10:30:00+09:00"); + const OriginalDate = Date; + + jest.spyOn(global, "Date").mockImplementation((value) => { + return value ? new OriginalDate(value) : new OriginalDate(mockDate); + }); + }); + + it("'모집'중인 방에 2일 이상 남으면 '리뷰 마감까지 남은 일', '모집 마감까지 남은 일'이 보인다", async () => { + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent("D-3"); + expect(reviewLeftDay).toHaveTextContent("D-6"); + }); + + it("'모집'이 24시간 미만, '리뷰'가 24시간 이상 남은 경우 '모집 마감까지 남은 시간', '리뷰 마감까지 남은 일'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-03T00:30:00+09:00", + reviewDeadline: "2024-10-05T00:30:00+09:00", + }; + + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent("14시간 0분 전"); + expect(reviewLeftDay).toHaveTextContent("D-2"); + }); + + it("'모집'이 24시간 미만, '리뷰'가 24시간 미만인 경우 '모집 마감까지 남은 시간', '리뷰 마감까지 남은 시간'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-02T12:30:00+09:00", + reviewDeadline: "2024-10-03T00:30:00+09:00", + }; + + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent("2시간 0분 전"); + expect(reviewLeftDay).toHaveTextContent("14시간 0분 전"); + }); + + it("'모집'완료 후 '진행 중'으로 바뀌었을 때 '리뷰'가 24시간 이상인 경우 '리뷰 마감까지 남은 일'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-01T12:30:00+09:00", + reviewDeadline: "2024-10-04T00:30:00+09:00", + }; + + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent(""); + expect(reviewLeftDay).toHaveTextContent("D-1"); + }); + + it("'모집'완료 후 '진행 중'으로 바뀌었을 때 '리뷰'가 24시간 미만인 경우 '리뷰 마감까지 남은 시간'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-01T12:30:00+09:00", + reviewDeadline: "2024-10-03T00:30:00+09:00", + }; + + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent(""); + expect(reviewLeftDay).toHaveTextContent("14시간 0분 전"); + }); + + it("'종료됨' 상태인 방에서는 남은 기간이 보이지 않는다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "CLOSE", + recruitmentDeadline: "2024-10-01T12:30:00+09:00", + reviewDeadline: "2024-10-03T00:30:00+09:00", + }; + + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent(""); + expect(reviewLeftDay).toHaveTextContent(""); + }); + + it("'실패' 상태인 방에서는 남은 기간이 보이지 않는다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "FAIL", + recruitmentDeadline: "2024-10-01T12:30:00+09:00", + reviewDeadline: "2024-10-03T00:30:00+09:00", + }; + + render( + + + , + ); + + const recruitLeftDay = screen.getByTestId("recruitLeftTime"); + const reviewLeftDay = screen.getByTestId("reviewLeftTime"); + + expect(recruitLeftDay).toHaveTextContent(""); + expect(reviewLeftDay).toHaveTextContent(""); + }); +}); diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx index 04ac35649..0196aadc6 100644 --- a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx @@ -68,7 +68,7 @@ const RoomInfoCard = ({ roomInfo }: { roomInfo: RoomInfo }) => { {formatDateTimeString(roomInfo.recruitmentDeadline)} - + {roomInfo.roomStatus === "OPEN" && formatDday(roomInfo.recruitmentDeadline) !== "종료됨" && displayLeftTime(roomInfo.recruitmentDeadline)} @@ -84,8 +84,9 @@ const RoomInfoCard = ({ roomInfo }: { roomInfo: RoomInfo }) => {
{formatDateTimeString(roomInfo.reviewDeadline)} - - {formatDday(roomInfo.reviewDeadline) !== "종료됨" && + + {(roomInfo.roomStatus === "OPEN" || roomInfo.roomStatus === "PROGRESS") && + formatDday(roomInfo.reviewDeadline) !== "종료됨" && displayLeftTime(roomInfo.reviewDeadline)}
diff --git a/frontend/src/components/shared/roomCard/RoomCard.test.tsx b/frontend/src/components/shared/roomCard/RoomCard.test.tsx new file mode 100644 index 000000000..b055bc2c8 --- /dev/null +++ b/frontend/src/components/shared/roomCard/RoomCard.test.tsx @@ -0,0 +1,298 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { ThemeProvider } from "styled-components"; +import RoomCard from "@/components/shared/roomCard/RoomCard"; +import { RoomInfo } from "@/@types/roomInfo"; +import { theme } from "@/styles/theme"; + +const mockBaseRoomInfo: RoomInfo = { + id: 1, + manager: "darr", + currentParticipants: 5, + roomStatus: "OPEN", + participationStatus: "PARTICIPATED", + memberRole: "BOTH", + title: "테스트 제목", + content: "테스트 본문", + repositoryLink: "테스트 링크", + thumbnailLink: "테스트 썸네일", + matchingSize: 5, + keywords: ["테스트"], + limitedParticipants: 10, + recruitmentDeadline: "2024-10-05T10:30:00+09:00", + reviewDeadline: "2024-10-08T10:30:00+09:00", +}; + +describe("RoomCard 컴포넌트 테스트", () => { + beforeAll(() => { + const mockDate = new Date("2024-10-02T10:30:00+09:00"); + const OriginalDate = Date; + + jest.spyOn(global, "Date").mockImplementation((value) => { + return value ? new OriginalDate(value) : new OriginalDate(mockDate); + }); + }); + + it("'모집'중인 방이 2일 이상 남으면 '모집마감 D-N'이 보인다", async () => { + render( + + + , + ); + + const text = await screen.findByText("모집 마감"); + const leftDay = await screen.findByText("D-3"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'모집'중인 방의 날짜가 1일이 남았고, 시간상으로 24시간 이상 남으면 'D-1'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-03T14:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("모집 마감"); + const leftDay = await screen.findByText("D-1"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'모집'중인 방의 날짜가 1일이 남았고, 시간상으로 24시간 미만 남으면 남은 시간이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-03T08:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("모집 마감"); + const leftDay = await screen.findByText("22시간 0분 전"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'모집'중인 방 당일인 경우 남은 시간이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-03T08:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("모집 마감"); + const leftDay = await screen.findByText("22시간 0분 전"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'모집'중인 방 당일인 경우 남은 시간이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-02T13:35:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("모집 마감"); + const leftDay = await screen.findByText("3시간 5분 전"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'모집'중인 방 당일 남은 시간이 없으면 '곧 종료'가 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + recruitmentDeadline: "2024-10-02T10:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("모집 마감"); + const leftDay = await screen.findByText("곧 종료"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'진행'중인 방에 2일 이상 남으면 '리뷰마감 D-N'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "PROGRESS", + }; + + render( + + + , + ); + + const text = await screen.findByText("리뷰 마감"); + const leftDay = await screen.findByText("D-6"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'진행'중인 방의 날짜가 1일이 남았고, 시간상으로 24시간 이상 남으면 'D-1'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "PROGRESS", + recruitmentDeadline: "2024-10-01T14:30:00+09:00", + reviewDeadline: "2024-10-03T14:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("리뷰 마감"); + const leftDay = await screen.findByText("D-1"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'진행'중인 방의 날짜가 1일이 남았고, 시간상으로 24시간 미만 남으면 남은 시간이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "PROGRESS", + recruitmentDeadline: "2024-10-01T14:30:00+09:00", + reviewDeadline: "2024-10-03T00:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("리뷰 마감"); + const leftDay = await screen.findByText("14시간 0분 전"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'진행'중인 방 당일인 경우 남은 시간이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "PROGRESS", + recruitmentDeadline: "2024-10-01T14:30:00+09:00", + reviewDeadline: "2024-10-02T15:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("리뷰 마감"); + const leftDay = await screen.findByText("5시간 0분 전"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'진행'중인 방 당일인 경우 남은 시간이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "PROGRESS", + recruitmentDeadline: "2024-10-01T14:30:00+09:00", + reviewDeadline: "2024-10-02T12:35:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("리뷰 마감"); + const leftDay = await screen.findByText("2시간 5분 전"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'진행'중인 방 당일 남은 시간이 없으면 '곧 종료'가 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "PROGRESS", + recruitmentDeadline: "2024-10-01T14:30:00+09:00", + reviewDeadline: "2024-10-02T10:30:00+09:00", + }; + render( + + + , + ); + + const text = await screen.findByText("리뷰 마감"); + const leftDay = await screen.findByText("곧 종료"); + + expect(text).toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'실패'된 방은 '종료됨'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "FAIL", + }; + render( + + + , + ); + + const leftDay = await screen.findByText("종료됨"); + const recruitText = screen.queryByText("모집 마감"); + const reviewText = screen.queryByText("리뷰 마감"); + + expect(recruitText).not.toBeInTheDocument(); + expect(reviewText).not.toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); + + it("'종료'된 방은 '종료됨'이 보인다", async () => { + const mockRoomInfo: RoomInfo = { + ...mockBaseRoomInfo, + roomStatus: "CLOSE", + }; + render( + + + , + ); + + const recruitText = screen.queryByText("모집 마감"); + const reviewText = screen.queryByText("리뷰 마감"); + const leftDay = await screen.findByText("종료됨"); + + expect(recruitText).not.toBeInTheDocument(); + expect(reviewText).not.toBeInTheDocument(); + expect(leftDay).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/shared/roomCard/RoomCard.tsx b/frontend/src/components/shared/roomCard/RoomCard.tsx index 5bca335bc..f1cf443a9 100644 --- a/frontend/src/components/shared/roomCard/RoomCard.tsx +++ b/frontend/src/components/shared/roomCard/RoomCard.tsx @@ -31,7 +31,7 @@ const DisplayLeftTime = (roomInfo: RoomInfo) => { return ( <> {dDay !== "종료됨" && "리뷰 마감"} - {dDay ? leftTime : dDay} + {dDay === "D-Day" ? leftTime : dDay} ); } diff --git a/frontend/src/utils/areDatesEqual.ts b/frontend/src/utils/areDatesEqual.ts deleted file mode 100644 index 0cc11720d..000000000 --- a/frontend/src/utils/areDatesEqual.ts +++ /dev/null @@ -1,9 +0,0 @@ -const areDatesEqual = (date1: Date, date2: Date): boolean => { - return ( - date1.getFullYear() === date2.getFullYear() && - date1.getMonth() === date2.getMonth() && - date1.getDate() === date2.getDate() - ); -}; - -export default areDatesEqual; diff --git a/frontend/src/utils/dateFormatter.test.ts b/frontend/src/utils/dateFormatter.test.ts new file mode 100644 index 000000000..bc4015534 --- /dev/null +++ b/frontend/src/utils/dateFormatter.test.ts @@ -0,0 +1,120 @@ +import { + displayLeftTime, + formatDateTimeString, + formatDday, + formatLeftTime, +} from "@/utils/dateFormatter"; + +describe("날짜 포메팅 유틸 함수 테스트", () => { + beforeAll(() => { + const mockDate = new Date("2024-10-02T10:30:00+09:00"); + const OriginalDate = Date; + + jest.spyOn(global, "Date").mockImplementation((value) => { + return value ? new OriginalDate(value) : new OriginalDate(mockDate); + }); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe("formatDateTimeString 함수 테스트", () => { + it("string 타입의 date를 받아 '연-월-일 시간:분' 포멧팅으로 반환 테스트", () => { + const mockDeadlineDate = "2024-10-14T12:56:31"; + const deadline = formatDateTimeString(mockDeadlineDate); + + expect(deadline).toBe("24-10-14 12:56"); + }); + }); + + describe("formatDday 함수 테스트", () => { + it("날짜가 5일이 남은 경우 'D-5'가 반환", () => { + const dateString = "2024-10-07T12:00:00+09:00"; + const dDay = formatDday(dateString); + + expect(dDay).toBe("D-5"); + }); + + it("날짜가 1일이 남았고, 시간상으로 24시간 이상 남은 경우 'D-1'이 반환", () => { + const dateString = "2024-10-03T10:35:00+09:00"; + const dDay = formatDday(dateString); + + expect(dDay).toBe("D-1"); + }); + + it("날짜가 1일 남았고, 시간상으로 24시간 이하로 남은 경우 'D-Day'가 반환", () => { + const dateString = "2024-10-03T10:29:00+09:00"; + const dDay = formatDday(dateString); + + expect(dDay).toBe("D-Day"); + }); + + it("날짜가 0일 남았고, 시간이 남은 경우 'D-Day'가 반환", () => { + const dateString = "2024-10-02T23:29:00+09:00"; + const dDay = formatDday(dateString); + + expect(dDay).toBe("D-Day"); + }); + + it("날짜가 0일 남았지만, 시간이 지난 경우 '종료됨'을 반환", () => { + const dateString = "2024-10-02T10:29:00+09:00"; + const dDay = formatDday(dateString); + + expect(dDay).toBe("종료됨"); + }); + + it("날짜가 지난 경우 '종료됨'을 반환", () => { + const dateString = "2024-10-01T10:35:00+09:00"; + const dDay = formatDday(dateString); + + expect(dDay).toBe("종료됨"); + }); + }); + + describe("formatLeftTime 함수 테스트", () => { + it("종료 시간이 1시간 이상이 남은 경우 'N시간 M분 전' 반환 테스트", () => { + const dateString = "2024-10-02T15:00:30+09:00"; + const leftTime = formatLeftTime(dateString); + + expect(leftTime).toBe("4시간 30분 전"); + }); + + it("종료 시간이 1시간 미만으로 남은 경우 'M분 전' 반환 테스트", () => { + const dateString = "2024-10-02T11:15:30+09:00"; + const leftTime = formatLeftTime(dateString); + + expect(leftTime).toBe("45분 전"); + }); + + it("종료 시간이 되었으면 '곧 종료' 반환 테스트", () => { + const dateString = "2024-10-02T10:30:30+09:00"; + const leftTime = formatLeftTime(dateString); + + expect(leftTime).toBe("곧 종료"); + }); + }); + + describe("displayLeftTime 함수 테스트", () => { + it("2일 이상 남은 경우 남은 디데이 반환 테스트", () => { + const dateString = "2024-10-05T10:30:30+09:00"; + const leftTime = displayLeftTime(dateString); + + expect(leftTime).toBe("D-3"); + }); + + it("날짜가 1일이 남았고, 시간상으로 24시간 이상 남은 경우 'D-1'이 반환", () => { + const dateString = "2024-10-03T10:31:30+09:00"; + const leftTime = displayLeftTime(dateString); + + expect(leftTime).toBe("D-1"); + }); + + it("날짜가 1일이 남았고, 시간상으로 24시간 미만 남은 경우 남은 시간이 반환", () => { + const dateString = "2024-10-03T10:29:30+09:00"; + const leftTime = displayLeftTime(dateString); + + expect(leftTime).toBe("23시간 59분 전"); + }); + }); +}); diff --git a/frontend/src/utils/dateFormatter.ts b/frontend/src/utils/dateFormatter.ts index 51a974e24..937e0c081 100644 --- a/frontend/src/utils/dateFormatter.ts +++ b/frontend/src/utils/dateFormatter.ts @@ -9,12 +9,6 @@ const formatStringToDate = ( return { year, month, day, hours, minutes }; }; -// 마감일 포맷 함수 -export const formatDeadlineString = (dateString: string): string => { - const { year, month, day, hours, minutes } = formatStringToDate(dateString); - return `${year.slice(2)}-${month}-${day} ${hours}:${minutes}`; -}; - // 일반 날짜 시간 포맷 함수 export const formatDateTimeString = (dateString: string): string => { const { year, month, day, hours, minutes } = formatStringToDate(dateString); @@ -24,18 +18,17 @@ export const formatDateTimeString = (dateString: string): string => { // 디데이 포맷 함수 export const formatDday = (dateString: string): string => { const targetDate = new Date(dateString); - const today = new Date(); + const now = new Date(); - today.setHours(0, 0, 0, 0); - targetDate.setHours(0, 0, 0, 0); + const timeDiff = targetDate.getTime() - now.getTime(); + const dayDiff = Math.floor(timeDiff / (1000 * 3600 * 24)); - const timeDiff = targetDate.getTime() - today.getTime(); - const dayDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)); + const hoursDiff = timeDiff / (1000 * 3600); - if (dayDiff > 0) { + if (dayDiff > 0 && hoursDiff >= 24) { return `D-${dayDiff}`; } - if (dayDiff === 0) { + if (dayDiff >= 0 && hoursDiff < 24) { return "D-Day"; } return "종료됨"; @@ -78,6 +71,14 @@ export const formatLeftTime = (time: string) => { return hours !== 0 ? `${hours}시간 ${minutes}분 전` : `${minutes}분 전`; }; +export const areDatesEqual = (date1: Date, date2: Date): boolean => { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ); +}; + const calculateTimeDifference = ( currentHourStr: string, currentMinuteStr: string, From 258afcc0d2b7826ba8406c314c77b0fb0989238d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 12:43:58 +0900 Subject: [PATCH 13/31] =?UTF-8?q?[FE]=20=EC=83=9D=EC=84=B1=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=8B=AC=EB=A0=A5,=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81(#6?= =?UTF-8?q?03)=20(#614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * design: 방 생성 페이지 모바일, 태블릿 대응 * refactor: error props 제거 * feat: DateTimePicker 생성하여 날짜, 시간 한번에 관리 * refactor: formatCombinedDateTime 인자 하나로 받기 * refactor: handleInputChange 함수로 통합하기 * refactor: 분류 type 맞추기 * feat: 방 생성 유효성 처리 --------- Co-authored-by: jinsil --- frontend/src/@types/roomInfo.ts | 14 +- frontend/src/apis/rooms.api.ts | 4 +- .../CalendarDropdown.stories.tsx | 2 - .../CalendarDropdown.style.ts | 4 +- .../calendarDropdown/CalendarDropdown.tsx | 7 +- .../common/dropdown/Dropdown.style.ts | 4 +- .../components/common/dropdown/Dropdown.tsx | 10 +- .../timeDropdown/TimeDropdown.stories.tsx | 31 ---- .../common/timeDropdown/TimeDropdown.style.ts | 4 +- .../common/timeDropdown/TimeDropdown.tsx | 9 +- .../dateTimePicker/DateTimePicker.tsx | 32 ++++ frontend/src/hooks/mutations/useMutateRoom.ts | 4 +- .../pages/roomCreate/RoomCreatePage.style.ts | 14 +- .../src/pages/roomCreate/RoomCreatePage.tsx | 153 +++++++----------- frontend/src/utils/dateFormatter.ts | 10 +- 15 files changed, 138 insertions(+), 164 deletions(-) create mode 100644 frontend/src/components/dateTimePicker/DateTimePicker.tsx diff --git a/frontend/src/@types/roomInfo.ts b/frontend/src/@types/roomInfo.ts index cbaea01b8..0f5271209 100644 --- a/frontend/src/@types/roomInfo.ts +++ b/frontend/src/@types/roomInfo.ts @@ -18,11 +18,17 @@ interface BaseRoomInfo { matchingSize: number; keywords: string[]; limitedParticipants: number; - recruitmentDeadline: string; - reviewDeadline: string; } export interface CreateRoomInfo extends BaseRoomInfo { - classification: Classification | ""; + recruitmentDeadline: Date; + reviewDeadline: Date; + classification: Classification; +} + +export interface SubmitRoomInfo extends BaseRoomInfo { + recruitmentDeadline: string; + reviewDeadline: string; + classification: Classification; } export interface RoomInfo extends BaseRoomInfo { @@ -32,6 +38,8 @@ export interface RoomInfo extends BaseRoomInfo { roomStatus: RoomStatus; participationStatus: ParticipationStatus; memberRole: Role; + recruitmentDeadline: string; + reviewDeadline: string; message: string; } diff --git a/frontend/src/apis/rooms.api.ts b/frontend/src/apis/rooms.api.ts index 614fa4543..a8b87a6d9 100644 --- a/frontend/src/apis/rooms.api.ts +++ b/frontend/src/apis/rooms.api.ts @@ -1,7 +1,7 @@ import apiClient from "./apiClient"; import { API_ENDPOINTS } from "./endpoints"; import { ParticipantListInfo } from "@/@types/participantList"; -import { CreateRoomInfo, Role, RoomInfo, RoomListInfo } from "@/@types/roomInfo"; +import { Role, RoomInfo, RoomListInfo, SubmitRoomInfo } from "@/@types/roomInfo"; import MESSAGES from "@/constants/message"; export const getParticipatedRoomList = async (): Promise => { @@ -58,7 +58,7 @@ export const getRoomDetailInfo = async (id: number): Promise => { return res; }; -export const postCreateRoom = async (roomData: CreateRoomInfo): Promise => { +export const postCreateRoom = async (roomData: SubmitRoomInfo): Promise => { return apiClient.post({ endpoint: API_ENDPOINTS.ROOMS, body: roomData, diff --git a/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx b/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx index fd9b145ca..ecadb43c7 100644 --- a/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx +++ b/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx @@ -74,7 +74,6 @@ export const 캘린더_드롭다운_에러: Story = { options: { isPastDateDisabled: true, }, - error: true, }, render: (args) => { const [selectedDate, setSelectedDate] = useState(args.selectedDate); @@ -88,7 +87,6 @@ export const 캘린더_드롭다운_에러: Story = { selectedDate={selectedDate} handleSelectedDate={handleSelectedDate} options={args.options} - error={args.error} /> ); }, diff --git a/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts b/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts index 73c9fbe97..18c99ccb7 100644 --- a/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts +++ b/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts @@ -5,7 +5,7 @@ export const CalendarDropdownContainer = styled.section` width: 130px; `; -export const CalendarDropdownToggle = styled.input<{ $error: boolean }>` +export const CalendarDropdownToggle = styled.input` cursor: pointer; width: 100%; @@ -15,7 +15,7 @@ export const CalendarDropdownToggle = styled.input<{ $error: boolean }>` text-align: center; letter-spacing: 0.2rem; - border: 1px solid ${(props) => (props.$error ? props.theme.COLOR.error : props.theme.COLOR.grey1)}; + border: 1px solid ${({ theme }) => theme.COLOR.grey1}; border-radius: 6px; outline-color: ${({ theme }) => theme.COLOR.black}; `; diff --git a/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx b/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx index 04f9ce8c0..2eb1ba8a5 100644 --- a/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx +++ b/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx @@ -4,16 +4,12 @@ import useDropdown from "@/hooks/common/useDropdown"; import Calendar, { CalendarProps } from "@/components/common/calendar/Calendar"; import { formatDate } from "@/utils/dateFormatter"; -type CalendarDropdownProps = CalendarProps & - InputHTMLAttributes & { - error?: boolean; - }; +type CalendarDropdownProps = CalendarProps & InputHTMLAttributes; const CalendarDropdown = ({ selectedDate, handleSelectedDate, options, - error = false, ...rest }: CalendarDropdownProps) => { const { isDropdownOpen, handleToggleDropdown, dropdownRef } = useDropdown(); @@ -32,7 +28,6 @@ const CalendarDropdown = ({ onClick={handleToggleDropdown} placeholder="날짜를 선택하세요" readOnly - $error={error} {...rest} /> {isDropdownOpen && ( diff --git a/frontend/src/components/common/dropdown/Dropdown.style.ts b/frontend/src/components/common/dropdown/Dropdown.style.ts index 50a81323a..2bd03cb25 100644 --- a/frontend/src/components/common/dropdown/Dropdown.style.ts +++ b/frontend/src/components/common/dropdown/Dropdown.style.ts @@ -18,7 +18,7 @@ export const DropdownContainer = styled.div` height: 40px; `; -export const DropdownToggle = styled.div` +export const DropdownToggle = styled.div<{ $error: boolean }>` display: flex; align-items: center; justify-content: space-between; @@ -30,7 +30,7 @@ export const DropdownToggle = styled.div` font: ${({ theme }) => theme.TEXT.small}; color: ${({ theme }) => theme.COLOR.grey4}; - border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border: 1px solid ${(props) => (props.$error ? props.theme.COLOR.error : props.theme.COLOR.grey1)}; border-radius: 6px; `; diff --git a/frontend/src/components/common/dropdown/Dropdown.tsx b/frontend/src/components/common/dropdown/Dropdown.tsx index cf84d0c9a..d4025efdd 100644 --- a/frontend/src/components/common/dropdown/Dropdown.tsx +++ b/frontend/src/components/common/dropdown/Dropdown.tsx @@ -11,9 +11,15 @@ interface DropdownProps { dropdownItems: DropdownItem[]; selectedCategory: string; onSelectCategory: (category: string) => void; + error?: boolean; } -const Dropdown = ({ dropdownItems, onSelectCategory, selectedCategory }: DropdownProps) => { +const Dropdown = ({ + dropdownItems, + onSelectCategory, + selectedCategory, + error = false, +}: DropdownProps) => { const { isDropdownOpen, handleToggleDropdown, dropdownRef } = useDropdown(); const handleDropdownItemClick = (category: string) => { @@ -23,7 +29,7 @@ const Dropdown = ({ dropdownItems, onSelectCategory, selectedCategory }: Dropdow return ( - + {dropdownItems.find((item) => item.value === selectedCategory)?.text || "선택해주세요"} diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx b/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx index 32716b43e..f80737ac8 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx @@ -6,13 +6,6 @@ const meta = { title: "Common/TimeDropdown", component: TimeDropdown, argTypes: { - error: { - control: { - type: "boolean", - }, - description: "시간 입력 오류 여부", - defaultValue: false, - }, selectedTime: { control: { type: "object", @@ -37,29 +30,6 @@ type Story = StoryObj; // 기본 스토리 (Default) export const Default: Story = { args: { - error: false, - selectedTime: new Date(), - onTimeChange: () => {}, - }, - render: (args) => { - const [time, setTime] = useState(args.selectedTime); - return ( - { - setTime(newTime); - args.onTimeChange(newTime); - }} - /> - ); - }, -}; - -// 에러 발생 시 -export const 에러일_때: Story = { - args: { - error: true, selectedTime: new Date(), onTimeChange: () => {}, }, @@ -67,7 +37,6 @@ export const 에러일_때: Story = { const [time, setTime] = useState(args.selectedTime); return ( { setTime(newTime); diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts b/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts index 95d07f6d1..63210c468 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts @@ -6,7 +6,7 @@ export const TimeDropdownContainer = styled.section` width: 100px; `; -export const TimeDropdownToggle = styled.input<{ $error: boolean }>` +export const TimeDropdownToggle = styled.input` cursor: pointer; width: 100%; @@ -16,7 +16,7 @@ export const TimeDropdownToggle = styled.input<{ $error: boolean }>` text-align: center; letter-spacing: 0.2rem; - border: 1px solid ${(props) => (props.$error ? props.theme.COLOR.error : props.theme.COLOR.grey1)}; + border: 1px solid ${({ theme }) => theme.COLOR.grey1}; border-radius: 6px; outline-color: ${({ theme }) => theme.COLOR.black}; `; diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.tsx b/frontend/src/components/common/timeDropdown/TimeDropdown.tsx index 8b9214b78..88eb8f439 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.tsx +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.tsx @@ -6,7 +6,6 @@ import { formatTime } from "@/utils/dateFormatter"; interface TimeDropdownProps extends InputHTMLAttributes { selectedTime: Date; onTimeChange: (time: Date) => void; - error?: boolean; } interface TimeDropdownChangeProps { @@ -72,12 +71,7 @@ const TimePicker = ({ ); }; -export const TimeDropdown = ({ - selectedTime, - onTimeChange, - error = false, - ...rest -}: TimeDropdownProps) => { +export const TimeDropdown = ({ selectedTime, onTimeChange, ...rest }: TimeDropdownProps) => { const { isDropdownOpen, handleToggleDropdown, dropdownRef } = useDropdown(); const handleTimeChange = ({ newTime, canCloseDropdown }: TimeDropdownChangeProps) => { @@ -94,7 +88,6 @@ export const TimeDropdown = ({ onClick={handleToggleDropdown} placeholder="시간을 선택하세요" readOnly - $error={error} {...rest} /> {isDropdownOpen && } diff --git a/frontend/src/components/dateTimePicker/DateTimePicker.tsx b/frontend/src/components/dateTimePicker/DateTimePicker.tsx new file mode 100644 index 000000000..0d9f15f9f --- /dev/null +++ b/frontend/src/components/dateTimePicker/DateTimePicker.tsx @@ -0,0 +1,32 @@ +import CalendarDropdown from "@/components/common/calendarDropdown/CalendarDropdown"; +import { TimeDropdown } from "@/components/common/timeDropdown/TimeDropdown"; + +interface DateTimePickerProps { + selectedDateTime: Date; + onDateTimeChange: (dateTime: Date) => void; +} + +const DateTimePicker = ({ selectedDateTime, onDateTimeChange }: DateTimePickerProps) => { + const handleDateChange = (newDate: Date) => { + const updatedDateTime = new Date(newDate); + updatedDateTime.setHours(selectedDateTime.getHours()); + updatedDateTime.setMinutes(selectedDateTime.getMinutes()); + onDateTimeChange(updatedDateTime); + }; + + const handleTimeChange = (newTime: Date) => { + const updatedDateTime = new Date(selectedDateTime); + updatedDateTime.setHours(newTime.getHours()); + updatedDateTime.setMinutes(newTime.getMinutes()); + onDateTimeChange(updatedDateTime); + }; + + return ( + <> + + + + ); +}; + +export default DateTimePicker; diff --git a/frontend/src/hooks/mutations/useMutateRoom.ts b/frontend/src/hooks/mutations/useMutateRoom.ts index 5b26d9bfb..f7cecb66a 100644 --- a/frontend/src/hooks/mutations/useMutateRoom.ts +++ b/frontend/src/hooks/mutations/useMutateRoom.ts @@ -1,7 +1,7 @@ import useToast from "../common/useToast"; import useMutateHandlers from "./useMutateHandlers"; import { useMutation } from "@tanstack/react-query"; -import { CreateRoomInfo, Role } from "@/@types/roomInfo"; +import { Role, SubmitRoomInfo } from "@/@types/roomInfo"; import { deleteParticipateIn, deleteParticipatedRoom, @@ -15,7 +15,7 @@ const useMutateRoom = () => { const { handleMutateError } = useMutateHandlers(); const postCreateRoomMutation = useMutation({ - mutationFn: (roomData: CreateRoomInfo) => postCreateRoom(roomData), + mutationFn: (roomData: SubmitRoomInfo) => postCreateRoom(roomData), onSuccess: () => { openToast(MESSAGES.SUCCESS.POST_CREATE_ROOM); }, diff --git a/frontend/src/pages/roomCreate/RoomCreatePage.style.ts b/frontend/src/pages/roomCreate/RoomCreatePage.style.ts index e192ff464..951021841 100644 --- a/frontend/src/pages/roomCreate/RoomCreatePage.style.ts +++ b/frontend/src/pages/roomCreate/RoomCreatePage.style.ts @@ -1,4 +1,5 @@ import styled from "styled-components"; +import media from "@/styles/media"; export const CreateSection = styled.section` display: flex; @@ -15,14 +16,21 @@ export const CreateSection = styled.section` box-shadow: ${({ theme }) => theme.BOX_SHADOW.regular}; `; -export const RowContainer = styled.p` +export const RowContainer = styled.div` display: flex; + flex-direction: column; gap: 2rem; - align-items: center; + align-items: flex-start; + width: 100%; + + ${media.large` + flex-direction: row; + align-items: center; + `} `; -export const ContentLabel = styled.div` +export const ContentLabel = styled.span` flex-shrink: 0; width: 250px; font: ${({ theme }) => theme.TEXT.medium_bold}; diff --git a/frontend/src/pages/roomCreate/RoomCreatePage.tsx b/frontend/src/pages/roomCreate/RoomCreatePage.tsx index 571952981..542d9fb42 100644 --- a/frontend/src/pages/roomCreate/RoomCreatePage.tsx +++ b/frontend/src/pages/roomCreate/RoomCreatePage.tsx @@ -3,29 +3,28 @@ import { useNavigate } from "react-router-dom"; import useModal from "@/hooks/common/useModal"; import useMutateRoom from "@/hooks/mutations/useMutateRoom"; import Button from "@/components/common/button/Button"; -import CalendarDropdown from "@/components/common/calendarDropdown/CalendarDropdown"; import ContentSection from "@/components/common/contentSection/ContentSection"; import Dropdown, { DropdownItem } from "@/components/common/dropdown/Dropdown"; import { Input } from "@/components/common/input/Input"; import ConfirmModal from "@/components/common/modal/confirmModal/ConfirmModal"; import { Textarea } from "@/components/common/textarea/Textarea"; -import { TimeDropdown } from "@/components/common/timeDropdown/TimeDropdown"; +import DateTimePicker from "@/components/dateTimePicker/DateTimePicker"; import * as S from "@/pages/roomCreate/RoomCreatePage.style"; import { Classification, CreateRoomInfo } from "@/@types/roomInfo"; import MESSAGES from "@/constants/message"; import { formatCombinedDateTime } from "@/utils/dateFormatter"; -const initialFormState: CreateRoomInfo = { +const initialFormState = { title: "", content: "", repositoryLink: "", thumbnailLink: "", - matchingSize: 0, + matchingSize: 1, keywords: [], - limitedParticipants: 0, - recruitmentDeadline: formatCombinedDateTime(new Date(), new Date()), - reviewDeadline: formatCombinedDateTime(new Date(), new Date()), - classification: "", + limitedParticipants: 1, + recruitmentDeadline: new Date(), + reviewDeadline: new Date(), + classification: "ALL" as Classification, }; const dropdownItems: DropdownItem[] = [ @@ -36,70 +35,34 @@ const dropdownItems: DropdownItem[] = [ const RoomCreatePage = () => { const navigate = useNavigate(); - const [isClickedButton, setIsClickedButton] = useState(false); const [formState, setFormState] = useState(initialFormState); - - const [recruitmentDate, setRecruitmentDate] = useState(new Date()); - const [reviewDate, setReviewDate] = useState(new Date()); - const [recruitmentTime, setRecruitmentTime] = useState(new Date()); - const [reviewTime, setReviewTime] = useState(new Date()); - const { postCreateRoomMutation } = useMutateRoom(); const { isModalOpen, handleOpenModal, handleCloseModal } = useModal(); - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - let newValue: string | string[] | number; - if (name === "keywords") { - newValue = value.split(",").map((keyword) => keyword.trim()); - } else if (name === "matchingSize" || name === "limitedParticipants") { - newValue = value === "" ? 0 : parseInt(value, 10); - } else { - newValue = value; - } + const handleInputChange = (name: K, value: CreateRoomInfo[K]) => { setFormState((prevState) => ({ ...prevState, - [name]: newValue, - })); - }; - - const updateFormStateWithDateTime = ( - field: "recruitmentDeadline" | "reviewDeadline", - date: Date, - time: Date, - ) => { - setFormState((prev) => ({ - ...prev, - [field]: formatCombinedDateTime(date, time), + [name]: value, })); }; - const handleRecruitmentDateChange = (date: Date) => { - setRecruitmentDate(date); - updateFormStateWithDateTime("recruitmentDeadline", date, recruitmentTime); - }; - - const handleRecruitmentTimeChange = (time: Date) => { - setRecruitmentTime(time); - updateFormStateWithDateTime("recruitmentDeadline", recruitmentDate, time); - }; - - const handleReviewDateChange = (date: Date) => { - setReviewDate(date); - updateFormStateWithDateTime("reviewDeadline", date, reviewTime); - }; - - const handleReviewTimeChange = (time: Date) => { - setReviewTime(time); - updateFormStateWithDateTime("reviewDeadline", reviewDate, time); - }; + const isFormValid = + formState.title !== "" && + formState.classification !== "ALL" && + formState.repositoryLink !== "" && + formState.recruitmentDeadline !== null && + formState.reviewDeadline !== null; const handleConfirm = () => { - postCreateRoomMutation.mutate(formState, { + const formattedFormState = { + ...formState, + recruitmentDeadline: formatCombinedDateTime(formState.recruitmentDeadline), + reviewDeadline: formatCombinedDateTime(formState.reviewDeadline), + }; + postCreateRoomMutation.mutate(formattedFormState, { onSuccess: () => navigate("/"), }); - setIsClickedButton(true); handleCloseModal(); }; @@ -123,7 +86,7 @@ const RoomCreatePage = () => { handleInputChange("title", e.target.value)} error={isClickedButton && formState.title === ""} required /> @@ -139,11 +102,9 @@ const RoomCreatePage = () => { dropdownItems={dropdownItems} selectedCategory={formState.classification} onSelectCategory={(value) => - setFormState((prevState) => ({ - ...prevState, - classification: value as Classification, - })) + handleInputChange("classification", value as Classification) } + error={isClickedButton && formState.classification === "ALL"} /> @@ -154,7 +115,7 @@ const RoomCreatePage = () => {