-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 진행중인 방 상세보기 api 개발 #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
81f326e
ca85acb
f6fc6db
d9b6f4e
c995797
d1a1ec6
6ebab18
659adec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<CurrentVote> currentVotes | ||
| ) { | ||
| public record CurrentVote( | ||
| String content, | ||
| int page, | ||
| boolean isOverview, | ||
| List<VoteItem> voteItems | ||
| ) { | ||
| public record VoteItem( | ||
| String itemName | ||
| ) {} | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<UserRoom> findByRoomId = userRoomCommandPort.findAllByRoomId(roomId); | ||
| RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId); | ||
|
|
||
| // 3. 투표 참여율이 가장 높은 투표 조회 | ||
| List<RoomPlayingDetailViewResponse.CurrentVote> 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<RoomPlayingDetailViewResponse.CurrentVote> 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| package konkuk.thip.vote.adapter.out.persistence; | ||
|
|
||
| import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 웹 레이어 DTO에 대한 의존성을 검토해보세요. Repository 레이어가 web response DTO( 🤖 Prompt for AI Agents |
||
| import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface VoteQueryRepository { | ||
|
|
||
| List<VoteJpaEntity> findVotesByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId); | ||
|
|
||
| List<RoomPlayingDetailViewResponse.CurrentVote> findTopParticipationVotesByRoom(Long roomId, int count); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<VoteJpaEntity> 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<RoomPlayingDetailViewResponse.CurrentVote> findTopParticipationVotesByRoom(Long roomId, int count) { | ||
| // 1. Fetch top votes by total participation count | ||
| List<VoteJpaEntity> 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<RoomPlayingDetailViewResponse.CurrentVote.VoteItem> 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(); | ||
| } | ||
|
Comment on lines
+50
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 복잡한 QueryDSL 쿼리의 N+1 문제 가능성
성능을 개선하기 위해 단일 쿼리로 모든 데이터를 조회하는 방식을 고려해보세요: @Override
public List<RoomPlayingDetailViewResponse.CurrentVote> findTopParticipationVotesByRoom(Long roomId, int count) {
- // 1. Fetch top votes by total participation count
- List<VoteJpaEntity> 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<RoomPlayingDetailViewResponse.CurrentVote.VoteItem> 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();
+ // Fetch all data in a single query with join fetch
+ List<VoteJpaEntity> topVotesWithItems = jpaQueryFactory
+ .selectFrom(vote)
+ .leftJoin(vote.voteItemJpaEntities, voteItem).fetchJoin()
+ .where(vote.roomJpaEntity.roomId.eq(roomId))
+ .orderBy(voteItem.count.sum().desc())
+ .limit(count)
+ .fetch();
+
+ // Group and transform data
+ return topVotesWithItems.stream()
+ .collect(Collectors.groupingBy(Function.identity()))
+ .entrySet().stream()
+ .map(entry -> {
+ VoteJpaEntity voteEntity = entry.getKey();
+ List<RoomPlayingDetailViewResponse.CurrentVote.VoteItem> voteItems =
+ voteEntity.getVoteItemJpaEntities().stream()
+ .sorted((a, b) -> Integer.compare(b.getCount(), a.getCount()))
+ .map(item -> new RoomPlayingDetailViewResponse.CurrentVote.VoteItem(item.getItemName()))
+ .toList();
+ return new RoomPlayingDetailViewResponse.CurrentVote(
+ voteEntity.getContent(),
+ voteEntity.getPage(),
+ voteEntity.isOverview(),
+ voteItems
+ );
+ })
+ .toList();
}
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seongjunnoh @hd0rable 현재 위쪽에서 이미 vote 테이블과 voteItem 테이블을 조인하지 않고 vote만 조회한 후, 각 Vote에 대해 개별적으로 voteItem을 조회하고 있습니다. 만약 양방향 매핑이 되어 있다면 현재는 상위 3개의 투표만 조회하므로 총 4개의 쿼리로 끝나지만, 이후 다른 기능들(특히 기록장 조회 API)에서도 Vote와 VoteItem은 자주 함께 사용될 가능성이 높아 보입니다. 따라서 성능 측면에서도 양방향 매핑을 추가한 뒤 fetch join을 고려하는 방식이 더 나을 수 있지 않을까 조심스럽게 제안드립니다..! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @buzz0331 정말 좋은 분석입니다! 맞습니다. 현재 구조에서는 N+1 문제가 발생하고 있으며, 양방향 매핑과 fetch join을 통한 해결 방안이 더 나은 접근법이라고 생각합니다. 특히 기록장 조회 API 등 다른 기능에서도 Vote와 VoteItem이 함께 사용될 가능성이 높다는 점을 고려하면, 장기적으로 보았을 때 양방향 매핑을 추가하는 것이 성능상 더 유리할 것 같습니다. 현재 3개의 투표만 조회하므로 4개의 쿼리로 제한되지만, 확장성을 고려한다면 좋은 개선 방향이라고 생각합니다! ✏️ Learnings added
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<RoomPlayingDetailViewResponse.CurrentVote> findTopParticipationVotesByRoom(Room room, int count); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻