diff --git a/docs/api-specs/home-api.md b/docs/api-specs/home-api.md index 9719ecdf..b4a770cc 100644 --- a/docs/api-specs/home-api.md +++ b/docs/api-specs/home-api.md @@ -13,6 +13,7 @@ - `editorPicks`: 에디터 픽 배틀 목록 - `trendingBattles`: 트렌딩 배틀 목록 - `bestBattles`: 베스트 배틀 목록 +- `mostDiscussedBattles`: 댓글 수가 많은 배틀 목록 - `todayQuizzes`: 오늘의 퀴즈 목록 - `todayVotes`: 오늘의 투표(Poll) 목록 - `newBattles`: 신규 배틀 목록 @@ -56,3 +57,6 @@ - 오늘의 퀴즈/투표는 서버에서 조회 및 정렬을 확정해 응답 - 옵션 순서는 `displayOrder -> label -> id` 기준 오름차순으로 고정 +- `bestBattles[].badgeLabel`은 `BEST`, `trendingBattles[].badgeLabel`은 `TRENDING`, `mostDiscussedBattles[].badgeLabel`은 `최다 토론`이다. +- 배틀 카드 응답은 태그와 소요 시간(초)을 포함한다. 에디터 픽에도 `audioDuration`을 제공한다. +- `mostDiscussedBattles`는 공개 배틀 중 댓글 수 내림차순으로 최대 3건이며 댓글이 없는 배틀은 제외한다. diff --git a/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java b/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java index 182f2379..2c6fd18a 100644 --- a/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java +++ b/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java @@ -47,6 +47,12 @@ public Battle toEntity(AdminBattleCreateRequest request, User admin) { } public TodayBattleResponse toTodayResponse(Battle battle, List tags, List options) { + return toTodayResponse(battle, tags, options, + battle.getCommentCount() == null ? 0L : battle.getCommentCount()); + } + + public TodayBattleResponse toTodayResponse(Battle battle, List tags, List options, + Long commentCount) { return new TodayBattleResponse( battle.getId(), battle.getTitle(), @@ -56,7 +62,8 @@ public TodayBattleResponse toTodayResponse(Battle battle, List tags, List tags, - List options -) {} + List options, + Long commentCount +) { + public TodayBattleResponse(Long battleId, String title, String summary, String thumbnailUrl, + Integer viewCount, Long participantsCount, Integer audioDuration, + List tags, List options) { + this(battleId, title, summary, thumbnailUrl, viewCount, participantsCount, audioDuration, + tags, options, 0L); + } +} diff --git a/src/main/java/com/swyp/picke/domain/battle/repository/BattleRepository.java b/src/main/java/com/swyp/picke/domain/battle/repository/BattleRepository.java index 647681b4..3587280a 100644 --- a/src/main/java/com/swyp/picke/domain/battle/repository/BattleRepository.java +++ b/src/main/java/com/swyp/picke/domain/battle/repository/BattleRepository.java @@ -38,6 +38,18 @@ public interface BattleRepository extends JpaRepository { "ORDER BY (battle.totalParticipantsCount + (battle.commentCount * 5)) DESC") List findBestBattles(Pageable pageable); + @Query(""" + SELECT battle, COUNT(comment) FROM Battle battle + LEFT JOIN Perspective perspective ON perspective.battle = battle + AND perspective.status = com.swyp.picke.domain.perspective.enums.PerspectiveStatus.PUBLISHED + LEFT JOIN PerspectiveComment comment ON comment.perspective = perspective AND comment.hidden = false + WHERE battle.status = 'PUBLISHED' AND battle.deletedAt IS NULL + GROUP BY battle + HAVING COUNT(comment) > 0 + ORDER BY COUNT(comment) DESC, battle.totalParticipantsCount DESC, battle.createdAt DESC + """) + List findMostDiscussedBattles(Pageable pageable); + // 4. 오늘의 Pické @Query("SELECT battle FROM Battle battle " + "WHERE battle.targetDate = :today " + diff --git a/src/main/java/com/swyp/picke/domain/battle/service/BattleService.java b/src/main/java/com/swyp/picke/domain/battle/service/BattleService.java index 776aa31d..e26c7a32 100644 --- a/src/main/java/com/swyp/picke/domain/battle/service/BattleService.java +++ b/src/main/java/com/swyp/picke/domain/battle/service/BattleService.java @@ -31,6 +31,8 @@ public interface BattleService { List getBestBattles(); + List getMostDiscussedBattles(); + List getTodayPicks(); List getNewBattles(List excludeIds); diff --git a/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java b/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java index 45c0088e..9b4f3718 100644 --- a/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java @@ -68,6 +68,7 @@ public class BattleServiceImpl implements BattleService { private static final int HOME_EDITOR_PICK_LIMIT = 10; private static final int HOME_TRENDING_LIMIT = 4; private static final int HOME_BEST_LIMIT = 3; + private static final int HOME_MOST_DISCUSSION_LIMIT = 3; private static final int HOME_TODAY_PICK_LIMIT = 1; private static final int HOME_NEW_LIMIT = 3; private static final int PUBLISH_BATCH_SIZE = 100; @@ -122,6 +123,15 @@ public List getBestBattles() { return loadBestBattles(HOME_BEST_LIMIT); } + @Override + public List getMostDiscussedBattles() { + List rows = battleRepository.findMostDiscussedBattles(PageRequest.of(0, HOME_MOST_DISCUSSION_LIMIT)); + List battles = rows.stream().map(row -> (Battle) row[0]).toList(); + Map commentCounts = rows.stream().collect(Collectors.toMap( + row -> ((Battle) row[0]).getId(), row -> ((Number) row[1]).longValue())); + return convertToTodayResponses(battles, commentCounts); + } + @Override @Transactional public List getTodayPicks() { @@ -640,6 +650,10 @@ public void afterCommit() { } private List convertToTodayResponses(List battles) { + return convertToTodayResponses(battles, Map.of()); + } + + private List convertToTodayResponses(List battles, Map commentCountOverrides) { if (battles == null || battles.isEmpty()) return Collections.emptyList(); Map> optionsMap = battleOptionRepository.findByBattleIn(battles) @@ -654,7 +668,10 @@ private List convertToTodayResponses(List battles) return battles.stream().map(battle -> { List tags = tagsMap.getOrDefault(battle.getId(), Collections.emptyList()); List options = optionsMap.getOrDefault(battle.getId(), Collections.emptyList()); - return battleConverter.toTodayResponse(battle, tags, options); + Long commentCount = commentCountOverrides.get(battle.getId()); + return commentCount == null + ? battleConverter.toTodayResponse(battle, tags, options) + : battleConverter.toTodayResponse(battle, tags, options, commentCount); }).toList(); } diff --git a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeBestBattleResponse.java b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeBestBattleResponse.java index b9c6d6b2..a23767f9 100644 --- a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeBestBattleResponse.java +++ b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeBestBattleResponse.java @@ -5,6 +5,7 @@ import java.util.List; public record HomeBestBattleResponse( + String badgeLabel, Long battleId, String philosopherA, String philosopherB, diff --git a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeEditorPickResponse.java b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeEditorPickResponse.java index efdc9bf5..49e2a3dc 100644 --- a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeEditorPickResponse.java +++ b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeEditorPickResponse.java @@ -12,5 +12,6 @@ public record HomeEditorPickResponse( String title, String summary, List tags, - Integer viewCount + Integer viewCount, + Integer audioDuration ) {} diff --git a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeMostDiscussedBattleResponse.java b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeMostDiscussedBattleResponse.java new file mode 100644 index 00000000..faea0439 --- /dev/null +++ b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeMostDiscussedBattleResponse.java @@ -0,0 +1,16 @@ +package com.swyp.picke.domain.home.dto.response; + +import com.swyp.picke.domain.battle.dto.response.BattleTagResponse; +import java.util.List; + +public record HomeMostDiscussedBattleResponse( + String badgeLabel, + Long battleId, + String thumbnailUrl, + String title, + List tags, + Integer audioDuration, + Long commentCount, + Integer viewCount +) { +} diff --git a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeResponse.java b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeResponse.java index a63f331c..d5383a5a 100644 --- a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeResponse.java +++ b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeResponse.java @@ -7,6 +7,7 @@ public record HomeResponse( List editorPicks, List trendingBattles, List bestBattles, + List mostDiscussedBattles, List todayQuizzes, List todayVotes, List newBattles diff --git a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeTrendingResponse.java b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeTrendingResponse.java index a8066b33..d3a9fe2e 100644 --- a/src/main/java/com/swyp/picke/domain/home/dto/response/HomeTrendingResponse.java +++ b/src/main/java/com/swyp/picke/domain/home/dto/response/HomeTrendingResponse.java @@ -5,6 +5,7 @@ import java.util.List; public record HomeTrendingResponse( + String badgeLabel, Long battleId, String thumbnailUrl, String title, diff --git a/src/main/java/com/swyp/picke/domain/home/service/HomeService.java b/src/main/java/com/swyp/picke/domain/home/service/HomeService.java index 032dd5cd..c7081957 100644 --- a/src/main/java/com/swyp/picke/domain/home/service/HomeService.java +++ b/src/main/java/com/swyp/picke/domain/home/service/HomeService.java @@ -7,6 +7,7 @@ import com.swyp.picke.domain.home.dto.response.HomeBestBattleResponse; import com.swyp.picke.domain.home.dto.response.HomeEditorPickResponse; import com.swyp.picke.domain.home.dto.response.HomeNewBattleResponse; +import com.swyp.picke.domain.home.dto.response.HomeMostDiscussedBattleResponse; import com.swyp.picke.domain.home.dto.response.HomeResponse; import com.swyp.picke.domain.home.dto.response.HomeTodayQuizResponse; import com.swyp.picke.domain.home.dto.response.HomeTodayVoteOptionResponse; @@ -57,10 +58,11 @@ public HomeResponse getHome(Long userId) { List editorPickRaw = battleService.getEditorPicks(); List trendingRaw = battleService.getTrendingBattles(); List bestRaw = battleService.getBestBattles(); + List mostDiscussedRaw = battleService.getMostDiscussedBattles(); List quizRaw = quizService.getTodayPicks(HOME_TODAY_PICK_LIMIT); List pollRaw = pollService.getTodayPicks(HOME_TODAY_PICK_LIMIT); - List excludeIds = collectBattleIds(editorPickRaw, trendingRaw, bestRaw); + List excludeIds = collectBattleIds(editorPickRaw, trendingRaw, bestRaw, mostDiscussedRaw); List newRaw = battleService.getNewBattles(excludeIds); return new HomeResponse( @@ -68,6 +70,7 @@ public HomeResponse getHome(Long userId) { editorPickRaw.stream().map(this::toEditorPick).toList(), trendingRaw.stream().map(this::toTrending).toList(), bestRaw.stream().map(this::toBestBattle).toList(), + mostDiscussedRaw.stream().map(this::toMostDiscussed).toList(), quizRaw.stream().map(this::toTodayQuiz).toList(), pollRaw.stream().map(this::toTodayVote).toList(), newRaw.stream().map(this::toNewBattle).toList() @@ -83,12 +86,14 @@ private HomeEditorPickResponse toEditorPick(TodayBattleResponse battle) { battle.title(), battle.summary(), battle.tags(), - battle.viewCount() + battle.viewCount(), + battle.audioDuration() ); } private HomeTrendingResponse toTrending(TodayBattleResponse battle) { return new HomeTrendingResponse( + "TRENDING", battle.battleId(), battle.thumbnailUrl(), battle.title(), @@ -100,6 +105,7 @@ private HomeTrendingResponse toTrending(TodayBattleResponse battle) { private HomeBestBattleResponse toBestBattle(TodayBattleResponse battle) { return new HomeBestBattleResponse( + "BEST", battle.battleId(), findOptionRepresentative(battle.options(), 0), findOptionRepresentative(battle.options(), 1), @@ -110,6 +116,19 @@ private HomeBestBattleResponse toBestBattle(TodayBattleResponse battle) { ); } + private HomeMostDiscussedBattleResponse toMostDiscussed(TodayBattleResponse battle) { + return new HomeMostDiscussedBattleResponse( + "최다 토론", + battle.battleId(), + battle.thumbnailUrl(), + battle.title(), + battle.tags(), + battle.audioDuration(), + battle.commentCount(), + battle.viewCount() + ); + } + private HomeTodayQuizResponse toTodayQuiz(Quiz quiz) { List options = quizService.getOptions(quiz); long participantsCount = quizService.countVotes(quiz);