Skip to content

Commit 5003613

Browse files
authored
#116 [Feat] 퀴즈 및 투표 API 구현, 배틀 리스트 타입 필터링 적용 및 성능 최적화 (#117)
1 parent d679d2f commit 5003613

10 files changed

Lines changed: 276 additions & 28 deletions

File tree

‎src/main/java/com/swyp/picke/domain/battle/repository/BattleRepository.java‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,38 @@
1515

1616
public interface BattleRepository extends JpaRepository<Battle, Long> {
1717

18-
// 1. EDITOR PICK
18+
// 1. EDITOR PICK - type 파라미터 추가
1919
@Query("SELECT battle FROM Battle battle " +
2020
"WHERE battle.isEditorPick = true AND battle.status = :status " +
21-
"AND battle.deletedAt IS NULL " +
21+
"AND battle.type = :type AND battle.deletedAt IS NULL " +
2222
"ORDER BY battle.createdAt DESC")
23-
List<Battle> findEditorPicks(@Param("status") BattleStatus status, Pageable pageable);
23+
List<Battle> findEditorPicks(@Param("status") BattleStatus status, @Param("type") BattleType type, Pageable pageable);
2424

25-
// 2. 지금 뜨는 배틀
25+
// 2. 지금 뜨는 배틀 - type 파라미터 추가
2626
@Query("SELECT battle FROM Battle battle JOIN Vote vote ON vote.battle = battle " +
27-
"WHERE vote.createdAt >= :yesterday AND battle.status = 'PUBLISHED' " +
28-
"AND battle.deletedAt IS NULL " +
27+
"WHERE vote.createdAt >= :yesterday AND battle.type = :type " +
28+
"AND battle.status = 'PUBLISHED' AND battle.deletedAt IS NULL " +
2929
"GROUP BY battle ORDER BY COUNT(vote) DESC")
30-
List<Battle> findTrendingBattles(@Param("yesterday") LocalDateTime yesterday, Pageable pageable);
30+
List<Battle> findTrendingBattles(@Param("yesterday") LocalDateTime yesterday, @Param("type") BattleType type, Pageable pageable);
3131

32-
// 3. Best 배틀
32+
// 3. Best 배틀 - type 파라미터 추가
3333
@Query("SELECT battle FROM Battle battle " +
34-
"WHERE battle.status = 'PUBLISHED' AND battle.deletedAt IS NULL " +
34+
"WHERE battle.status = 'PUBLISHED' AND battle.type = :type AND battle.deletedAt IS NULL " +
3535
"ORDER BY (battle.totalParticipantsCount + (battle.commentCount * 5)) DESC")
36-
List<Battle> findBestBattles(Pageable pageable);
36+
List<Battle> findBestBattles(@Param("type") BattleType type, Pageable pageable);
3737

3838
// 4. 오늘의 Pické
3939
@Query("SELECT battle FROM Battle battle " +
4040
"WHERE battle.type = :type AND battle.targetDate = :today " +
4141
"AND battle.status = 'PUBLISHED' AND battle.deletedAt IS NULL")
4242
List<Battle> findTodayPicks(@Param("type") BattleType type, @Param("today") LocalDate today, Pageable pageable);
4343

44-
// 5. 새로운 배틀
44+
// 5. 새로운 배틀 - type 파라미터 추가
4545
@Query("SELECT battle FROM Battle battle " +
46-
"WHERE battle.id NOT IN :excludeIds AND battle.status = 'PUBLISHED' " +
47-
"AND battle.deletedAt IS NULL " +
46+
"WHERE battle.id NOT IN :excludeIds AND battle.type = :type " +
47+
"AND battle.status = 'PUBLISHED' AND battle.deletedAt IS NULL " +
4848
"ORDER BY battle.createdAt DESC")
49-
List<Battle> findNewBattlesExcluding(@Param("excludeIds") List<Long> excludeIds, Pageable pageable);
49+
List<Battle> findNewBattlesExcluding(@Param("excludeIds") List<Long> excludeIds, @Param("type") BattleType type, Pageable pageable);
5050

5151
// 6. 전체 배틀 목록 조회 (페이징, 삭제된 항목 제외, 최신순)
5252
Page<Battle> findByDeletedAtIsNullOrderByCreatedAtDesc(Pageable pageable);
@@ -55,6 +55,8 @@ public interface BattleRepository extends JpaRepository<Battle, Long> {
5555
// 기본 조회용
5656
List<Battle> findByTargetDateAndStatusAndDeletedAtIsNull(LocalDate date, BattleStatus status);
5757

58+
List<Battle> findByTargetDateAndStatusAndTypeAndDeletedAtIsNull(LocalDate targetDate, BattleStatus status, BattleType type);
59+
5860
// 탐색 탭: 전체 배틀 검색 (정렬은 Pageable Sort로 처리)
5961
@Query("SELECT b FROM Battle b WHERE b.status = 'PUBLISHED' AND b.deletedAt IS NULL")
6062
List<Battle> searchAll(Pageable pageable);

‎src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,20 @@ public Battle findById(Long battleId) {
6969

7070
@Override
7171
public List<TodayBattleResponse> getEditorPicks(int limit) {
72-
List<Battle> battles = battleRepository.findEditorPicks(BattleStatus.PUBLISHED, PageRequest.of(0, limit));
72+
List<Battle> battles = battleRepository.findEditorPicks(BattleStatus.PUBLISHED, BattleType.BATTLE, PageRequest.of(0, limit));
7373
return convertToTodayResponses(battles);
7474
}
7575

7676
@Override
7777
public List<TodayBattleResponse> getTrendingBattles(int limit) {
7878
LocalDateTime yesterday = LocalDateTime.now().minusDays(1);
79-
List<Battle> battles = battleRepository.findTrendingBattles(yesterday, PageRequest.of(0, limit));
79+
List<Battle> battles = battleRepository.findTrendingBattles(yesterday, BattleType.BATTLE, PageRequest.of(0, limit));
8080
return convertToTodayResponses(battles);
8181
}
8282

8383
@Override
8484
public List<TodayBattleResponse> getBestBattles(int limit) {
85-
List<Battle> battles = battleRepository.findBestBattles(PageRequest.of(0, limit));
85+
List<Battle> battles = battleRepository.findBestBattles(BattleType.BATTLE, PageRequest.of(0, limit));
8686
return convertToTodayResponses(battles);
8787
}
8888

@@ -96,7 +96,7 @@ public List<TodayBattleResponse> getTodayPicks(BattleType type, int limit) {
9696
public List<TodayBattleResponse> getNewBattles(List<Long> excludeIds, int limit) {
9797
List<Long> finalExcludeIds = (excludeIds == null || excludeIds.isEmpty())
9898
? List.of(-1L) : excludeIds;
99-
List<Battle> battles = battleRepository.findNewBattlesExcluding(finalExcludeIds, PageRequest.of(0, limit));
99+
List<Battle> battles = battleRepository.findNewBattlesExcluding(finalExcludeIds, BattleType.BATTLE, PageRequest.of(0, limit));
100100
return convertToTodayResponses(battles);
101101
}
102102

@@ -127,8 +127,8 @@ public BattleListResponse getBattles(int page, int size, String type) {
127127

128128
@Override
129129
public TodayBattleListResponse getTodayBattles() {
130-
List<Battle> battles = battleRepository.findByTargetDateAndStatusAndDeletedAtIsNull(
131-
LocalDate.now(), BattleStatus.PUBLISHED);
130+
List<Battle> battles = battleRepository.findByTargetDateAndStatusAndTypeAndDeletedAtIsNull(
131+
LocalDate.now(), BattleStatus.PUBLISHED, BattleType.BATTLE);
132132

133133
List<Battle> limitedBattles = battles.stream()
134134
.limit(5)

‎src/main/java/com/swyp/picke/domain/vote/controller/VoteController.java‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.swyp.picke.domain.vote.controller;
22

3+
import com.swyp.picke.domain.vote.dto.request.QuizVoteRequest;
34
import com.swyp.picke.domain.vote.dto.request.VoteRequest;
4-
import com.swyp.picke.domain.vote.dto.response.MyVoteResponse;
5-
import com.swyp.picke.domain.vote.dto.response.VoteResultResponse;
6-
import com.swyp.picke.domain.vote.dto.response.VoteStatsResponse;
5+
import com.swyp.picke.domain.vote.dto.response.*;
6+
import com.swyp.picke.domain.vote.service.QuizVoteService;
77
import com.swyp.picke.domain.vote.service.VoteService;
88
import com.swyp.picke.global.common.response.ApiResponse;
99
import io.swagger.v3.oas.annotations.Operation;
@@ -19,9 +19,48 @@
1919
@RequiredArgsConstructor
2020
public class VoteController {
2121

22+
// 배틀(BATTLE) 전용 서비스
2223
private final VoteService voteService;
24+
// 퀴즈(QUIZ) & 투표(POLL) 전용 서비스
25+
private final QuizVoteService quizVoteService;
2326

24-
@Operation(summary = "사전 투표 실행", description = "배틀 진입 시 첫 투표(사전 투표)를 진행합니다.")
27+
@Operation(summary = "[퀴즈] 선택 제출")
28+
@PostMapping("/battles/{battleId}/quiz-vote")
29+
public ApiResponse<QuizVoteResponse> submitQuiz(
30+
@PathVariable Long battleId,
31+
@AuthenticationPrincipal Long userId,
32+
@RequestBody QuizVoteRequest request) {
33+
return ApiResponse.onSuccess(quizVoteService.submitQuiz(battleId, userId, request));
34+
}
35+
36+
@Operation(summary = "[투표] 선택 제출")
37+
@PostMapping("/battles/{battleId}/poll-vote")
38+
public ApiResponse<PollVoteResponse> submitPoll(
39+
@PathVariable Long battleId,
40+
@AuthenticationPrincipal Long userId,
41+
@RequestBody QuizVoteRequest request) {
42+
return ApiResponse.onSuccess(quizVoteService.submitPoll(battleId, userId, request));
43+
}
44+
45+
@Operation(summary = "[퀴즈] 내 퀴즈 참여 내역 조회", description = "내가 선택한 퀴즈 옵션과 통계를 조회합니다.")
46+
@GetMapping("/battles/{battleId}/quiz-vote/me")
47+
public ApiResponse<QuizVoteResponse> getMyQuizVote(
48+
@PathVariable Long battleId,
49+
@AuthenticationPrincipal Long userId) {
50+
return ApiResponse.onSuccess(quizVoteService.getMyQuizVote(battleId, userId));
51+
}
52+
53+
@Operation(summary = "[투표] 내 투표 참여 내역 조회", description = "내가 선택한 투표 옵션과 통계를 조회합니다.")
54+
@GetMapping("/battles/{battleId}/poll-vote/me")
55+
public ApiResponse<PollVoteResponse> getMyPollVote(
56+
@PathVariable Long battleId,
57+
@AuthenticationPrincipal Long userId) {
58+
return ApiResponse.onSuccess(quizVoteService.getMyPollVote(battleId, userId));
59+
}
60+
61+
// 2. 배틀(BATTLE) 관련 API
62+
63+
@Operation(summary = "[배틀] 사전 투표 실행", description = "배틀 진입 시 첫 투표(사전 투표)를 진행합니다.")
2564
@PostMapping("/battles/{battleId}/votes/pre")
2665
public ApiResponse<VoteResultResponse> preVote(
2766
@PathVariable Long battleId,
@@ -30,7 +69,7 @@ public ApiResponse<VoteResultResponse> preVote(
3069
return ApiResponse.onSuccess(voteService.preVote(battleId, userId, request));
3170
}
3271

33-
@Operation(summary = "사후 투표 실행", description = "콘텐츠 소비 후 최종 투표(사후 투표)를 진행합니다.")
72+
@Operation(summary = "[배틀] 사후 투표 실행", description = "콘텐츠 소비 후 최종 투표(사후 투표)를 진행합니다.")
3473
@PostMapping("/battles/{battleId}/votes/post")
3574
public ApiResponse<VoteResultResponse> postVote(
3675
@PathVariable Long battleId,
@@ -39,21 +78,21 @@ public ApiResponse<VoteResultResponse> postVote(
3978
return ApiResponse.onSuccess(voteService.postVote(battleId, userId, request));
4079
}
4180

42-
@Operation(summary = "투표 통계 조회", description = "특정 배틀의 옵션별 투표 수와 비율을 조회합니다.")
81+
@Operation(summary = "[배틀] 투표 통계 조회", description = "특정 배틀의 옵션별 투표 수와 비율을 조회합니다.")
4382
@GetMapping("/battles/{battleId}/vote-stats")
4483
public ApiResponse<VoteStatsResponse> getVoteStats(@PathVariable Long battleId) {
4584
return ApiResponse.onSuccess(voteService.getVoteStats(battleId));
4685
}
4786

48-
@Operation(summary = "내 투표 내역 조회", description = "특정 배틀에 대한 내 사전/사후 투표 내역과 현재 상태를 조회합니다.")
87+
@Operation(summary = "[배틀] 내 투표 내역 조회", description = "특정 배틀에 대한 내 사전/사후 투표 내역과 현재 상태를 조회합니다.")
4988
@GetMapping("/battles/{battleId}/votes/me")
5089
public ApiResponse<MyVoteResponse> getMyVote(
5190
@PathVariable Long battleId,
5291
@AuthenticationPrincipal Long userId) {
5392
return ApiResponse.onSuccess(voteService.getMyVote(battleId, userId));
5493
}
5594

56-
@Operation(summary = "오디오(TTS) 청취 완료 처리", description = "사전 투표 후, 오디오 재생이 완료되었을 때 호출하여 상태를 업데이트합니다.")
95+
@Operation(summary = "[배틀] 오디오(TTS) 청취 완료 처리", description = "사전 투표 후, 오디오 재생이 완료되었을 때 호출하여 상태를 업데이트합니다.")
5796
@PostMapping("/battles/{battleId}/votes/tts-complete")
5897
public ApiResponse<Void> completeTts(
5998
@PathVariable Long battleId,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.swyp.picke.domain.vote.dto.request;
2+
3+
public record QuizVoteRequest(
4+
Long optionId
5+
) {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.swyp.picke.domain.vote.dto.response;
2+
3+
import java.util.List;
4+
5+
public record PollVoteResponse(
6+
Long battleId,
7+
Long selectedOptionId,
8+
List<OptionStat> stats
9+
) {
10+
public record OptionStat(Long optionId, String label, String title, long voteCount, double ratio) {}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.swyp.picke.domain.vote.dto.response;
2+
3+
import java.util.List;
4+
5+
public record QuizVoteResponse(
6+
Long battleId,
7+
Long selectedOptionId,
8+
List<OptionStat> stats
9+
) {
10+
public record OptionStat(Long optionId, String label, String title, Boolean isCorrect, long voteCount, double ratio) {}
11+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.swyp.picke.domain.vote.entity;
2+
3+
import com.swyp.picke.domain.battle.entity.Battle;
4+
import com.swyp.picke.domain.battle.entity.BattleOption;
5+
import com.swyp.picke.domain.user.entity.User;
6+
import com.swyp.picke.global.common.BaseEntity;
7+
import jakarta.persistence.*;
8+
import lombok.AccessLevel;
9+
import lombok.Builder;
10+
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
12+
13+
@Getter
14+
@Entity
15+
@Table(name = "quiz_votes")
16+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
17+
public class QuizVote extends BaseEntity {
18+
19+
@ManyToOne(fetch = FetchType.LAZY)
20+
@JoinColumn(name = "user_id", nullable = false)
21+
private User user;
22+
23+
@ManyToOne(fetch = FetchType.LAZY)
24+
@JoinColumn(name = "battle_id", nullable = false)
25+
private Battle battle;
26+
27+
@ManyToOne(fetch = FetchType.LAZY)
28+
@JoinColumn(name = "option_id", nullable = false)
29+
private BattleOption selectedOption;
30+
31+
@Builder
32+
public QuizVote(User user, Battle battle, BattleOption selectedOption) {
33+
this.user = user;
34+
this.battle = battle;
35+
this.selectedOption = selectedOption;
36+
}
37+
38+
public void updateOption(BattleOption option) {
39+
this.selectedOption = option;
40+
}
41+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.swyp.picke.domain.vote.repository;
2+
3+
import com.swyp.picke.domain.battle.entity.Battle;
4+
import com.swyp.picke.domain.battle.entity.BattleOption;
5+
import com.swyp.picke.domain.user.entity.User;
6+
import com.swyp.picke.domain.vote.entity.QuizVote;
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
9+
import java.util.Optional;
10+
11+
public interface QuizVoteRepository extends JpaRepository<QuizVote, Long> {
12+
Optional<QuizVote> findByBattleAndUser(Battle battle, User user);
13+
long countByBattle(Battle battle);
14+
long countByBattleAndSelectedOption(Battle battle, BattleOption option);
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.swyp.picke.domain.vote.service;
2+
3+
import com.swyp.picke.domain.vote.dto.request.QuizVoteRequest;
4+
import com.swyp.picke.domain.vote.dto.response.PollVoteResponse;
5+
import com.swyp.picke.domain.vote.dto.response.QuizVoteResponse;
6+
7+
public interface QuizVoteService {
8+
QuizVoteResponse submitQuiz(Long battleId, Long userId, QuizVoteRequest request);
9+
PollVoteResponse submitPoll(Long battleId, Long userId, QuizVoteRequest request);
10+
QuizVoteResponse getMyQuizVote(Long battleId, Long userId);
11+
PollVoteResponse getMyPollVote(Long battleId, Long userId);
12+
}

0 commit comments

Comments
 (0)