From 81f326ee3d8e02c5290e700d92b7a34eb4a0e30e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 14 Jul 2025 12:30:22 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[feat]=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20controller=20=EA=B0=9C=EB=B0=9C=20(#?= =?UTF-8?q?74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/RoomQueryController.java | 17 +++++++-- .../RoomPlayingDetailViewResponse.java | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 4d68835cb..91f7f3cdd 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -2,15 +2,13 @@ import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse; import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse; import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; -import konkuk.thip.room.application.port.in.RoomGetHomeJoinedListUseCase; -import konkuk.thip.room.application.port.in.RoomSearchUseCase; +import konkuk.thip.room.application.port.in.*; import jakarta.validation.Valid; import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; -import konkuk.thip.room.application.port.in.RoomShowRecruitingDetailViewUseCase; -import konkuk.thip.room.application.port.in.RoomVerifyPasswordUseCase; import konkuk.thip.room.application.port.in.dto.RoomGetHomeJoinedListQuery; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -26,6 +24,7 @@ public class RoomQueryController { private final RoomVerifyPasswordUseCase roomVerifyPasswordUseCase; private final RoomShowRecruitingDetailViewUseCase roomShowRecruitingDetailViewUseCase; private final RoomGetHomeJoinedListUseCase roomGetHomeJoinedListUseCase; + private final RoomShowPlayingDetailViewUseCase roomShowPlayingDetailViewUseCase; @GetMapping("/rooms/search") public BaseResponse searchRooms( @@ -45,6 +44,7 @@ public BaseResponse verifyRoomPassword(@PathVariable("roomId") final Long return BaseResponse.ok(roomVerifyPasswordUseCase.verifyRoomPassword(roomVerifyPasswordRequest.toQuery(roomId))); } + // 모집중인 방 상세보기 @GetMapping("/rooms/{roomId}/recruiting") public BaseResponse getRecruitingRoomDetailView( @UserId final Long userId, @@ -62,4 +62,13 @@ public BaseResponse getHomeJoinedRooms(@UserId fi .page(page).build())); } + // 진행중인 방 상세보기 + @GetMapping("/rooms/{roomId}/playing") + public BaseResponse getPlayingRoomDetailView( + @UserId final Long userId, + @PathVariable("roomId") final Long roomId + ) { + return BaseResponse.ok(roomShowPlayingDetailViewUseCase.getPlayingRoomDetailView(userId, roomId)); + } + } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java new file mode 100644 index 000000000..e38152d34 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java @@ -0,0 +1,37 @@ +package konkuk.thip.room.adapter.in.web.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record RoomPlayingDetailViewResponse( + boolean isHost, + Long roomId, + String roomName, + String roomImageUrl, + boolean isPublic, + String progressStartDate, + String progressEndDate, + String category, + String roomDescription, + int memberCount, + int recruitCount, + String isbn, + String bookTitle, + String authorName, + int currentPage, + double userPercentage, + List currentVotes +) { + public record CurrentVote( + String content, + int page, + boolean isOverview, + List voteItems + ) { + public record VoteItem( + String itemName + ) {} + } +} From ca85acb6a14d73431ab065d2039d401982f120b4 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 14 Jul 2025 12:30:41 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[feat]=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20use=20case=20=EA=B0=9C=EB=B0=9C=20(#?= =?UTF-8?q?74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/RoomShowPlayingDetailViewUseCase.java | 8 +++ .../RoomShowPlayingDetailViewService.java | 71 +++++++++++++++++++ .../thip/user/domain/RoomParticipants.java | 19 +++++ 3 files changed, 98 insertions(+) create mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java create mode 100644 src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java new file mode 100644 index 000000000..abc8f2624 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; + +public interface RoomShowPlayingDetailViewUseCase { + + RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId); +} diff --git a/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java b/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java new file mode 100644 index 000000000..05318680d --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java @@ -0,0 +1,71 @@ +package konkuk.thip.room.application.service; + +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; +import konkuk.thip.common.util.DateUtil; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.application.port.in.RoomShowPlayingDetailViewUseCase; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; +import konkuk.thip.user.application.port.out.UserRoomCommandPort; +import konkuk.thip.user.domain.RoomParticipants; +import konkuk.thip.user.domain.UserRoom; +import konkuk.thip.vote.application.port.out.VoteQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RoomShowPlayingDetailViewService implements RoomShowPlayingDetailViewUseCase { + + private static final int TOP_PARTICIPATION_VOTES_COUNT = 3; + + private final RoomCommandPort roomCommandPort; + private final BookCommandPort bookCommandPort; + private final UserRoomCommandPort userRoomCommandPort; + private final VoteQueryPort voteQueryPort; + + @Override + @Transactional(readOnly = true) + public RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId) { + // 1. Room 조회, Book 조회 + Room room = roomCommandPort.findById(roomId); + Book book = bookCommandPort.findById(room.getBookId()); + + // 2. Room과 연관된 UserRoom 조회, RoomParticipants 일급 컬렉션 생성 + // TODO. Room 도메인에 memberCount 값 추가된 후 리펙토링 + List findByRoomId = userRoomCommandPort.findAllByRoomId(roomId); + RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId); + + // 3. 투표 참여율이 가장 높은 투표 조회 + List topParticipationVotes = voteQueryPort.findTopParticipationVotesByRoom(room, TOP_PARTICIPATION_VOTES_COUNT); + + // 4. response 구성 + return buildResponse(userId, room, book, roomParticipants, topParticipationVotes); + } + + private RoomPlayingDetailViewResponse buildResponse(Long userId, Room room, Book book, RoomParticipants roomParticipants, List topParticipationVotes) { + return RoomPlayingDetailViewResponse.builder() + .isHost(roomParticipants.isHostOfRoom(userId)) + .roomId(room.getId()) + .roomName(room.getTitle()) + .roomImageUrl(room.getCategory().getImageUrl()) + .isPublic(room.isPublic()) + .progressStartDate(DateUtil.formatDate(room.getStartDate())) + .progressEndDate(DateUtil.formatDate(room.getEndDate())) + .category(room.getCategory().getValue()) + .roomDescription(room.getDescription()) + .memberCount(roomParticipants.calculateMemberCount()) + .recruitCount(room.getRecruitCount()) + .isbn(book.getIsbn()) + .bookTitle(book.getTitle()) + .authorName(book.getAuthorName()) + .currentPage(roomParticipants.getCurrentPageOfUser(userId)) + .userPercentage(roomParticipants.getUserPercentageOfUser(userId)) + .currentVotes(topParticipationVotes) + .build(); + } +} diff --git a/src/main/java/konkuk/thip/user/domain/RoomParticipants.java b/src/main/java/konkuk/thip/user/domain/RoomParticipants.java index bceacbf17..5249b1b21 100644 --- a/src/main/java/konkuk/thip/user/domain/RoomParticipants.java +++ b/src/main/java/konkuk/thip/user/domain/RoomParticipants.java @@ -1,11 +1,14 @@ package konkuk.thip.user.domain; +import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.user.adapter.out.jpa.UserRoomRole; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.util.List; +import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_BELONG_TO_ROOM; + @Getter @RequiredArgsConstructor public class RoomParticipants { @@ -33,4 +36,20 @@ public boolean isHostOfRoom(Long userId) { .filter(userRoom -> userRoom.getUserId().equals(userId)) .anyMatch(userRoom -> userRoom.getUserRoomRole().equals(UserRoomRole.HOST.getType())); } + + public int getCurrentPageOfUser(Long userId) { + return participants.stream() + .filter(userRoom -> userRoom.getUserId().equals(userId)) + .map(UserRoom::getCurrentPage) + .findFirst() + .orElseThrow(() -> new InvalidStateException(USER_NOT_BELONG_TO_ROOM)); + } + + public double getUserPercentageOfUser(Long userId) { + return participants.stream() + .filter(userRoom -> userRoom.getUserId().equals(userId)) + .map(UserRoom::getUserPercentage) + .findFirst() + .orElseThrow(() -> new InvalidStateException(USER_NOT_BELONG_TO_ROOM)); + } } From f6fc6db0b1ff957998be64afcacc9fdf73b41dfa Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 14 Jul 2025 12:30:54 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[feat]=20error=20code=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 8d363aa16..9643169cf 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -98,6 +98,7 @@ public enum ErrorCode implements ResponseCode { * 140000 : userRoom error */ USER_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 140000, "존재하지 않는 USER_ROOM (방과 사용자 관계) 입니다."), + USER_NOT_BELONG_TO_ROOM(HttpStatus.BAD_REQUEST, 140001, "현재 모임방에 속하지 않는 유저입니다."), /** * 150000 : Category error From d9b6f4efdef1655625ec8dca9911213f941d2a39 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 14 Jul 2025 12:31:13 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[feat]=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20api=20=EC=98=81=EC=86=8D=EC=84=B1=20adapte?= =?UTF-8?q?r=20=EA=B0=9C=EB=B0=9C=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoteQueryPersistenceAdapter.java | 9 ++++ .../out/persistence/VoteQueryRepository.java | 3 ++ .../persistence/VoteQueryRepositoryImpl.java | 44 +++++++++++++++++-- .../application/port/out/VoteQueryPort.java | 6 +++ 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryPersistenceAdapter.java index e14eebee4..cc29dbb87 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryPersistenceAdapter.java @@ -1,10 +1,14 @@ package konkuk.thip.vote.adapter.out.persistence; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.domain.Room; import konkuk.thip.vote.adapter.out.mapper.VoteMapper; import konkuk.thip.vote.application.port.out.VoteQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository @RequiredArgsConstructor public class VoteQueryPersistenceAdapter implements VoteQueryPort { @@ -17,4 +21,9 @@ public class VoteQueryPersistenceAdapter implements VoteQueryPort { public boolean isUserVoted(Long userId, Long voteItemId) { return userVoteJpaRepository.existsByUserJpaEntity_UserIdAndVoteItemJpaEntity_VoteItemId(userId, voteItemId); } + + @Override + public List findTopParticipationVotesByRoom(Room room, int count) { + return voteJpaRepository.findTopParticipationVotesByRoom(room.getId(), count); + } } diff --git a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepository.java b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepository.java index 92c4463cb..736aea3f5 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepository.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepository.java @@ -1,5 +1,6 @@ package konkuk.thip.vote.adapter.out.persistence; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; import java.util.List; @@ -7,4 +8,6 @@ public interface VoteQueryRepository { List findVotesByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId); + + List findTopParticipationVotesByRoom(Long roomId, int count); } diff --git a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java index 27543256c..98f5b96ef 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java @@ -2,7 +2,9 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; +import konkuk.thip.vote.adapter.out.jpa.QVoteItemJpaEntity; import konkuk.thip.vote.adapter.out.jpa.QVoteJpaEntity; import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; import lombok.RequiredArgsConstructor; @@ -16,11 +18,12 @@ public class VoteQueryRepositoryImpl implements VoteQueryRepository { private final JPAQueryFactory jpaQueryFactory; + private final QVoteJpaEntity vote = QVoteJpaEntity.voteJpaEntity; + private final QUserJpaEntity user = QUserJpaEntity.userJpaEntity; + private final QVoteItemJpaEntity voteItem = QVoteItemJpaEntity.voteItemJpaEntity; + @Override public List findVotesByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId) { - QVoteJpaEntity vote = QVoteJpaEntity.voteJpaEntity; - QUserJpaEntity user = QUserJpaEntity.userJpaEntity; - return jpaQueryFactory .select(vote) .from(vote) @@ -43,4 +46,39 @@ private BooleanExpression filterByType(String type, QVoteJpaEntity post, Long us } return null; } + + @Override + public List findTopParticipationVotesByRoom(Long roomId, int count) { + // 1. Fetch top votes by total participation count + List topVotes = jpaQueryFactory + .select(vote) + .from(vote) + .leftJoin(voteItem).on(voteItem.voteJpaEntity.eq(vote)) + .where(vote.roomJpaEntity.roomId.eq(roomId)) + .groupBy(vote) + .orderBy(voteItem.count.sum().desc()) // 해당 투표에 참여한 총 참여자 수 기준 내림차순 정렬 + .limit(count) + .fetch(); + + // 2. Map to DTOs including vote items + return topVotes.stream() + .map(vote -> { + List voteItems = jpaQueryFactory + .select(voteItem) + .from(voteItem) + .where(voteItem.voteJpaEntity.eq(vote)) + .orderBy(voteItem.count.desc()) + .fetch() + .stream() + .map(item -> new RoomPlayingDetailViewResponse.CurrentVote.VoteItem(item.getItemName())) + .toList(); + return new RoomPlayingDetailViewResponse.CurrentVote( + vote.getContent(), + vote.getPage(), + vote.isOverview(), + voteItems + ); + }) + .toList(); + } } diff --git a/src/main/java/konkuk/thip/vote/application/port/out/VoteQueryPort.java b/src/main/java/konkuk/thip/vote/application/port/out/VoteQueryPort.java index e2655502e..73b820288 100644 --- a/src/main/java/konkuk/thip/vote/application/port/out/VoteQueryPort.java +++ b/src/main/java/konkuk/thip/vote/application/port/out/VoteQueryPort.java @@ -1,7 +1,13 @@ package konkuk.thip.vote.application.port.out; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.domain.Room; + +import java.util.List; + public interface VoteQueryPort { boolean isUserVoted(Long userId, Long voteId); + List findTopParticipationVotesByRoom(Room room, int count); } From c9957973b86bbb08c6afd339ce7f45c52d52f56d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 14 Jul 2025 12:31:34 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[test]=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20api=20=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/RoomPlayingDetailViewApiTest.java | 379 ++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java new file mode 100644 index 000000000..9f8128629 --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java @@ -0,0 +1,379 @@ +package konkuk.thip.room.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.util.DateUtil; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; +import konkuk.thip.user.adapter.out.jpa.*; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserRoomJpaRepository; +import konkuk.thip.vote.adapter.out.jpa.VoteItemJpaEntity; +import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; +import konkuk.thip.vote.adapter.out.persistence.VoteItemJpaRepository; +import konkuk.thip.vote.adapter.out.persistence.VoteJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.IntStream; + +import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_BELONG_TO_ROOM; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 진행 중인 방 상세조회 api 통합 테스트") +class RoomPlayingDetailViewApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private CategoryJpaRepository categoryJpaRepository; + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private RoomJpaRepository roomJpaRepository; + + @Autowired + private UserRoomJpaRepository userRoomJpaRepository; + + @Autowired + private VoteJpaRepository voteJpaRepository; + + @Autowired + private VoteItemJpaRepository voteItemJpaRepository; + + @AfterEach + void tearDown() { + voteItemJpaRepository.deleteAll(); + voteJpaRepository.deleteAll(); + userRoomJpaRepository.deleteAll(); + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + + BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() + .title(bookTitle) + .isbn(isbn) + .authorName("한강") + .bestSeller(false) + .publisher("문학동네") + .imageUrl("https://image1.jpg") + .pageCount(300) + .description("한강의 소설") + .build()); + + CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(alias)); + + return roomJpaRepository.save(RoomJpaEntity.builder() + .title(roomName) + .description("한강 작품 읽기 모임") + .isPublic(true) + .roomPercentage(0.0) + .startDate(startDate) + .endDate(LocalDate.now().plusDays(30)) + .recruitCount(recruitCount) + .bookJpaEntity(book) + .categoryJpaEntity(category) + .build()); + } + + private void saveUsersToRoom(RoomJpaEntity roomJpaEntity, int count) { + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + + // User 리스트 생성 및 저장 + List users = IntStream.rangeClosed(1, count) + .mapToObj(i -> UserJpaEntity.builder() + .nickname("user" + i) + .imageUrl("http://image") + .oauth2Id("oauth2Id") + .role(UserRole.USER) + .aliasForUserJpaEntity(alias) + .build()) + .toList(); + + List savedUsers = userJpaRepository.saveAll(users); + + // UserRoom 매핑 리스트 생성 및 저장 + List mappings = savedUsers.stream() + .map(user -> UserRoomJpaEntity.builder() + .userJpaEntity(user) + .roomJpaEntity(roomJpaEntity) + .userRoomRole(UserRoomRole.MEMBER) + .build()) + .toList(); + + userRoomJpaRepository.saveAll(mappings); + } + + private void createVoteToRoom(UserJpaEntity creator, RoomJpaEntity roomJpaEntity, int count) { + for (int v = 1; v <= count; v++) { + VoteJpaEntity voteJpaEntity = voteJpaRepository.save( + VoteJpaEntity.builder() + .content("vote-content-" + v) + .likeCount(0) + .commentCount(0) + .userJpaEntity(creator) + .page(v * 10) + .isOverview(false) + .roomJpaEntity(roomJpaEntity) + .build() + ); + + for (int vi = 1; vi <= 2; vi++) { + voteItemJpaRepository.save( + VoteItemJpaEntity.builder() + .itemName("item-" + v + "-" + vi) + .count(v * 10) // v값이 클수록 해당 투표의 투표항목을 선택한 사람 수가 많다 == 해당 투표의 참여율이 높다 + .voteJpaEntity(voteJpaEntity) + .build() + ); + } + } + } + + @Test + @DisplayName("진행중인 모임방 상세조회할 경우, [해당 모임방의 정보, 책 정보, 유저의 현재 활동 정보, 현재 진행중인 투표]를 반환한다.") + void get_playing_room_detail() throws Exception { + //given + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + saveUsersToRoom(room, 4); + UserRoomJpaEntity userRoomJpaEntity = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(room.getRoomId()).get(0); + userRoomJpaRepository.delete(userRoomJpaEntity); + UserRoomJpaEntity joiningMember = userRoomJpaRepository.save(UserRoomJpaEntity.builder() + .userJpaEntity(userRoomJpaEntity.getUserJpaEntity()) + .roomJpaEntity(userRoomJpaEntity.getRoomJpaEntity()) + .userRoomRole(UserRoomRole.MEMBER) // Member + .currentPage(50) // 현재 member의 마지막 활동 page + .userPercentage(10.6) // 현재 member의 활동 percentage + .build()); + + createVoteToRoom(joiningMember.getUserJpaEntity(), room, 2); // 2개의 투표 생성 + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isHost", is(false))) + .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomImageUrl", is("과학/IT_image"))) // 방 대표 이미지 추가 + .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1))))) + .andExpect(jsonPath("$.data.memberCount", is(4))) + .andExpect(jsonPath("$.data.recruitCount", is(10))) + .andExpect(jsonPath("$.data.isbn", is("isbn1"))) + .andExpect(jsonPath("$.data.bookTitle", is("과학-책"))) + .andExpect(jsonPath("$.data.currentPage", is(50))) + .andExpect(jsonPath("$.data.userPercentage", is(10.6))) + .andExpect(jsonPath("$.data.currentVotes", hasSize(2))) + /** + * currentVotes 검증 : 현재 모임방의 참여율이 높은 투표와 투표 항목들을 노출 + * <정렬 순서> : 투표 참여율 높은 순 (vote 2 -> vote 1 순) + */ + .andExpect(jsonPath("$.data.currentVotes[0].content", is("vote-content-2"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[0].itemName", is("item-2-1"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[1].itemName", is("item-2-2"))) + + .andExpect(jsonPath("$.data.currentVotes[1].content", is("vote-content-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[0].itemName", is("item-1-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[1].itemName", is("item-1-2"))); + } + + @Test + @DisplayName("모임방의 호스트가 조회할 경우, 유저가 해당 방의 호스트임을 응답값으로 보여준다. (나머지 응답값은 동일)") + void get_playing_room_detail_host() throws Exception { + //given + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + saveUsersToRoom(room, 4); + UserRoomJpaEntity userRoomJpaEntity = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(room.getRoomId()).get(0); + userRoomJpaRepository.delete(userRoomJpaEntity); + UserRoomJpaEntity roomHost = userRoomJpaRepository.save(UserRoomJpaEntity.builder() + .userJpaEntity(userRoomJpaEntity.getUserJpaEntity()) + .roomJpaEntity(userRoomJpaEntity.getRoomJpaEntity()) + .userRoomRole(UserRoomRole.HOST) // HOST + .currentPage(50) // 현재 member의 마지막 활동 page + .userPercentage(10.6) // 현재 member의 활동 percentage + .build()); + + createVoteToRoom(roomHost.getUserJpaEntity(), room, 2); // 2개의 투표 생성 + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + .requestAttr("userId", roomHost.getUserJpaEntity().getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isHost", is(true))) // 방 HOST 이면 true + .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomImageUrl", is("과학/IT_image"))) // 방 대표 이미지 추가 + .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1))))) + .andExpect(jsonPath("$.data.memberCount", is(4))) + .andExpect(jsonPath("$.data.recruitCount", is(10))) + .andExpect(jsonPath("$.data.isbn", is("isbn1"))) + .andExpect(jsonPath("$.data.bookTitle", is("과학-책"))) + .andExpect(jsonPath("$.data.currentPage", is(50))) + .andExpect(jsonPath("$.data.userPercentage", is(10.6))) + .andExpect(jsonPath("$.data.currentVotes", hasSize(2))) + /** + * currentVotes 검증 : 현재 모임방의 참여율이 높은 투표와 투표 항목들을 노출 + * <정렬 순서> : 투표 참여율 높은 순 (vote 2 -> vote 1 순) + */ + .andExpect(jsonPath("$.data.currentVotes[0].content", is("vote-content-2"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[0].itemName", is("item-2-1"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[1].itemName", is("item-2-2"))) + + .andExpect(jsonPath("$.data.currentVotes[1].content", is("vote-content-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[0].itemName", is("item-1-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[1].itemName", is("item-1-2"))); + } + + @Test + @DisplayName("모임방에 속하지 않는 유저가 진행중인 모임방 상세조회를 요청한 경우, 400 error 발생한다.") + void get_playing_room_detail_not_belong_to_room() throws Exception { + //given + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + saveUsersToRoom(room, 4); + UserRoomJpaEntity userRoomJpaEntity = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(room.getRoomId()).get(0); + userRoomJpaRepository.delete(userRoomJpaEntity); + UserRoomJpaEntity joiningMember = userRoomJpaRepository.save(UserRoomJpaEntity.builder() + .userJpaEntity(userRoomJpaEntity.getUserJpaEntity()) + .roomJpaEntity(userRoomJpaEntity.getRoomJpaEntity()) + .userRoomRole(UserRoomRole.MEMBER) // Member + .currentPage(50) // 현재 member의 마지막 활동 page + .userPercentage(10.6) // 현재 member의 활동 percentage + .build()); + + createVoteToRoom(joiningMember.getUserJpaEntity(), room, 2); // 2개의 투표 생성 + + //when //then + mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + .requestAttr("userId", 1000L)) // 방에 속하지 않는 유저 + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(USER_NOT_BELONG_TO_ROOM.getCode())) + .andExpect(jsonPath("$.message", containsString(USER_NOT_BELONG_TO_ROOM.getMessage()))); + } + + @Test + @DisplayName("모임방에서 진행중인 투표가 많을 경우, 참여율이 높은 순으로 최대 3개의 투표만 보여준다.") + void get_playing_room_detail_too_many_votes() throws Exception { + //given + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + saveUsersToRoom(room, 4); + UserRoomJpaEntity userRoomJpaEntity = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(room.getRoomId()).get(0); + userRoomJpaRepository.delete(userRoomJpaEntity); + UserRoomJpaEntity joiningMember = userRoomJpaRepository.save(UserRoomJpaEntity.builder() + .userJpaEntity(userRoomJpaEntity.getUserJpaEntity()) + .roomJpaEntity(userRoomJpaEntity.getRoomJpaEntity()) + .userRoomRole(UserRoomRole.MEMBER) // Member + .currentPage(50) // 현재 member의 마지막 활동 page + .userPercentage(10.6) // 현재 member의 활동 percentage + .build()); + + createVoteToRoom(joiningMember.getUserJpaEntity(), room, 6); // 6개의 투표 생성 + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isHost", is(false))) + .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomImageUrl", is("과학/IT_image"))) // 방 대표 이미지 추가 + .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1))))) + .andExpect(jsonPath("$.data.memberCount", is(4))) + .andExpect(jsonPath("$.data.recruitCount", is(10))) + .andExpect(jsonPath("$.data.isbn", is("isbn1"))) + .andExpect(jsonPath("$.data.bookTitle", is("과학-책"))) + .andExpect(jsonPath("$.data.currentPage", is(50))) + .andExpect(jsonPath("$.data.userPercentage", is(10.6))) + .andExpect(jsonPath("$.data.currentVotes", hasSize(3))) + /** + * currentVotes 검증 : 현재 모임방의 참여율이 높은 투표와 투표 항목들을 노출 + * <정렬 순서> : 투표 참여율 높은 순 (vote 6 -> vote 5 -> vote 4 순) + */ + .andExpect(jsonPath("$.data.currentVotes[0].content", is("vote-content-6"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[0].itemName", is("item-6-1"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[1].itemName", is("item-6-2"))) + + .andExpect(jsonPath("$.data.currentVotes[1].content", is("vote-content-5"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[0].itemName", is("item-5-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[1].itemName", is("item-5-2"))) + + .andExpect(jsonPath("$.data.currentVotes[2].content", is("vote-content-4"))) + .andExpect(jsonPath("$.data.currentVotes[2].voteItems[0].itemName", is("item-4-1"))) + .andExpect(jsonPath("$.data.currentVotes[2].voteItems[1].itemName", is("item-4-2"))); + } + + @Test + @DisplayName("모임방에서 진행중인 투표가 없을 경우, 빈 리스트를 보여준다.") + void get_playing_room_detail_no_votes() throws Exception { + //given + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + saveUsersToRoom(room, 4); + UserRoomJpaEntity userRoomJpaEntity = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(room.getRoomId()).get(0); + userRoomJpaRepository.delete(userRoomJpaEntity); + UserRoomJpaEntity joiningMember = userRoomJpaRepository.save(UserRoomJpaEntity.builder() + .userJpaEntity(userRoomJpaEntity.getUserJpaEntity()) + .roomJpaEntity(userRoomJpaEntity.getRoomJpaEntity()) + .userRoomRole(UserRoomRole.MEMBER) // Member + .currentPage(50) // 현재 member의 마지막 활동 page + .userPercentage(10.6) // 현재 member의 활동 percentage + .build()); + + createVoteToRoom(joiningMember.getUserJpaEntity(), room, 0); // 투표 생성 X + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isHost", is(false))) + .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomImageUrl", is("과학/IT_image"))) // 방 대표 이미지 추가 + .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1))))) + .andExpect(jsonPath("$.data.memberCount", is(4))) + .andExpect(jsonPath("$.data.recruitCount", is(10))) + .andExpect(jsonPath("$.data.isbn", is("isbn1"))) + .andExpect(jsonPath("$.data.bookTitle", is("과학-책"))) + .andExpect(jsonPath("$.data.currentPage", is(50))) + .andExpect(jsonPath("$.data.userPercentage", is(10.6))) + .andExpect(jsonPath("$.data.currentVotes", hasSize(0))); // 투표 없음 + } +} From d1a1ec6dcbde1bd23abea2dcbe391d817fe4d707 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 14 Jul 2025 12:31:50 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[test]=20RoomParticipants=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/domain/RoomParticipantsTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/test/java/konkuk/thip/user/domain/RoomParticipantsTest.java b/src/test/java/konkuk/thip/user/domain/RoomParticipantsTest.java index 88fc566ed..8b5fbd89a 100644 --- a/src/test/java/konkuk/thip/user/domain/RoomParticipantsTest.java +++ b/src/test/java/konkuk/thip/user/domain/RoomParticipantsTest.java @@ -1,5 +1,6 @@ package konkuk.thip.user.domain; +import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.user.adapter.out.jpa.UserRoomRole; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -61,4 +62,54 @@ void is_host_of_room_test() { assertFalse(participants.isHostOfRoom(1L)); assertFalse(participants.isHostOfRoom(3L)); } + + @Test + @DisplayName("유저가 현재 모임방에서 마지막으로 활동한 페이지(= currentPage) 를 반환한다.") + void get_current_page_of_user_test() { + //given + UserRoom ur = createUserRoom(1L, 123L, 10L, UserRoomRole.MEMBER); + RoomParticipants participants = RoomParticipants.from(List.of(ur)); + + //when + int page = participants.getCurrentPageOfUser(123L); + + //then + assertEquals(0, page); + } + + @Test + @DisplayName("현재 모임방에 속하지 않는 유저가 getCurrentPageOfUser 메서드를 호출하면, InvalidStateException이 발생한다.") + void get_current_page_of_user_not_belong_test() { + //given + UserRoom ur = createUserRoom(1L, 123L, 10L, UserRoomRole.MEMBER); + RoomParticipants participants = RoomParticipants.from(List.of(ur)); + + //when & then + assertThrows(InvalidStateException.class, () -> participants.getCurrentPageOfUser(999L)); + } + + @Test + @DisplayName("유저가 현재 모임방에서 활동한 percentage(= userPercentage) 를 반환한다.") + void get_user_percentage_of_user_test() { + //given + UserRoom ur = createUserRoom(1L, 123L, 10L, UserRoomRole.MEMBER); + RoomParticipants participants = RoomParticipants.from(List.of(ur)); + + //when + double percentage = participants.getUserPercentageOfUser(123L); + + //then + assertEquals(0.0, percentage); + } + + @Test + @DisplayName("현재 모임방에 속하지 않는 유저가 getUserPercentageOfUser 메서드를 호출하면, InvalidStateException이 발생한다.") + void get_user_percentage_of_user_not_belong_test() { + //given + UserRoom ur = createUserRoom(1L, 123L, 10L, UserRoomRole.MEMBER); + RoomParticipants participants = RoomParticipants.from(List.of(ur)); + + //when & then + assertThrows(InvalidStateException.class, () -> participants.getUserPercentageOfUser(999L)); + } } From 659adec15e68b625dcf93e333e882530810e87cb Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 14 Jul 2025 23:44:47 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[fix]=20=EC=A4=91=EB=B3=B5=20DI=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/room/adapter/in/web/RoomQueryController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 43d96439d..d185ed994 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -29,7 +29,6 @@ public class RoomQueryController { private final RoomVerifyPasswordUseCase roomVerifyPasswordUseCase; private final RoomShowRecruitingDetailViewUseCase roomShowRecruitingDetailViewUseCase; private final RoomGetMemberListUseCase roomGetMemberListUseCase; - private final RoomGetHomeJoinedListUseCase roomGetHomeJoinedListUseCase; private final RoomShowPlayingDetailViewUseCase roomShowPlayingDetailViewUseCase; @GetMapping("/rooms/search")