diff --git a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc index bb18132..acb0977 100644 --- a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc @@ -490,3 +490,75 @@ include::{snippets}/get-vote-statistics-fail-not-found/http-request.adoc[] include::{snippets}/get-vote-statistics-fail-not-found/http-response.adoc[] ==== + +== 대회 전체 팀 조회 + +=== `GET`: 대회의 팀 목록 조회 (비회원용) + +NOTE: 비회원용 메인 페이지 API입니다. `isLiked`와 `isVoted` 값이 항상 `false` 입니다. + +.Path Parameters +include::{snippets}/get-contest-team-summaries-public/path-parameters.adoc[] + +.HTTP Request +include::{snippets}/get-contest-team-summaries-public/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries-public/http-response.adoc[] + +.Response Body's Fields +include::{snippets}/get-contest-team-summaries-public/response-fields.adoc[] + +=== `GET`: 대회의 팀 목록 조회 (회원용 - 미투표 기간) + +NOTE: 회원만 접근 가능한 메인 페이지용 API입니다. 미투표 기간에는 로그인한 사용자의 `isLiked` 여부가 포함됩니다. + +.Path Parameters +include::{snippets}/get-contest-team-summaries-member-non-voting/path-parameters.adoc[] + +.Request Headers +include::{snippets}/get-contest-team-summaries-member-non-voting/request-headers.adoc[] + +.HTTP Request +include::{snippets}/get-contest-team-summaries-member-non-voting/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries-member-non-voting/http-response.adoc[] + +.Response Body's Fields +include::{snippets}/get-contest-team-summaries-member-non-voting/response-fields.adoc[] + +=== `GET`: 대회의 팀 목록 조회 (회원용 - 투표 기간) + +NOTE: 회원만 접근 가능한 메인 페이지용 API입니다. 투표 기간에는 로그인한 사용자의 `isVoted` 여부가 포함됩니다. + +.Path Parameters +include::{snippets}/get-contest-team-summaries-member-voting/path-parameters.adoc[] + +.Request Headers +include::{snippets}/get-contest-team-summaries-member-voting/request-headers.adoc[] + +.HTTP Request +include::{snippets}/get-contest-team-summaries-member-voting/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries-member-voting/http-response.adoc[] + +.Response Body's Fields +include::{snippets}/get-contest-team-summaries-member-voting/response-fields.adoc[] + +==== ⚠️ 실패 케이스 + +.❌ Case 1: 존재하지 않는 대회 ID + +[%collapsible] +==== +.HTTP Request +include::{snippets}/get-contest-team-summaries-fail-contest-not-found/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries-fail-contest-not-found/http-response.adoc[] + +.Path Parameters +include::{snippets}/get-contest-team-summaries-fail-contest-not-found/path-parameters.adoc[] +==== diff --git a/src/main/java/com/opus/opus/modules/contest/api/ContestController.java b/src/main/java/com/opus/opus/modules/contest/api/ContestController.java index 2e43f52..ff296cc 100644 --- a/src/main/java/com/opus/opus/modules/contest/api/ContestController.java +++ b/src/main/java/com/opus/opus/modules/contest/api/ContestController.java @@ -11,17 +11,18 @@ import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; import com.opus.opus.modules.contest.application.dto.response.ContestCurrentResponse; import com.opus.opus.modules.contest.application.dto.response.ContestCurrentToggleResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import com.opus.opus.modules.contest.application.dto.response.ContestResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSortResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSubmissionResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; -import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.PositiveOrZero; import java.util.List; @@ -205,4 +206,20 @@ public ResponseEntity> getTeamSubmissions(@PathV final List responses = contestQueryService.getTeamSubmissions(contestId); return ResponseEntity.ok(responses); } + + @GetMapping("/{contestId}/teams") + public ResponseEntity> getAllContestTeamSummaries( + @PathVariable final Long contestId, + @LoginMember final Member member + ) { + final List responses = contestQueryService.getContestTeamSummaries(contestId, member); + return ResponseEntity.ok(responses); + } + + @GetMapping("/{contestId}/teams/public") + public ResponseEntity> getAllContestTeamSummariesPublic( + @PathVariable final Long contestId) { + final List responses = contestQueryService.getContestTeamSummariesPublic(contestId); + return ResponseEntity.ok(responses); + } } diff --git a/src/main/java/com/opus/opus/modules/contest/application/ContestQueryService.java b/src/main/java/com/opus/opus/modules/contest/application/ContestQueryService.java index fd22f99..689a8cf 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/ContestQueryService.java +++ b/src/main/java/com/opus/opus/modules/contest/application/ContestQueryService.java @@ -7,6 +7,7 @@ import static org.springframework.data.domain.Sort.Direction.DESC; import com.opus.opus.global.util.FileStorageUtil; +import com.opus.opus.modules.contest.application.convenience.ContestAwardConvenience; import com.opus.opus.modules.contest.application.convenience.ContestCategoryConvenience; import com.opus.opus.modules.contest.application.convenience.ContestConvenience; import com.opus.opus.modules.contest.application.convenience.ContestSortConvenience; @@ -19,7 +20,10 @@ import com.opus.opus.modules.contest.application.dto.response.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; +import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; import com.opus.opus.modules.contest.domain.Contest; +import com.opus.opus.modules.contest.domain.ContestAward; import com.opus.opus.modules.contest.domain.ContestCategory; import com.opus.opus.modules.contest.domain.ContestSort; import com.opus.opus.modules.contest.domain.ContestTrack; @@ -32,9 +36,15 @@ import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.application.convenience.TeamConvenience; import com.opus.opus.modules.team.application.convenience.TeamVoteConvenience; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.modules.team.application.convenience.TeamConvenience; +import com.opus.opus.modules.team.application.convenience.TeamLikeConvenience; +import com.opus.opus.modules.team.application.convenience.TeamVoteConvenience; import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamVote; +import com.opus.opus.modules.team.domain.Team; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; @@ -46,6 +56,7 @@ import java.util.stream.Collectors; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.antlr.v4.runtime.misc.Pair; import org.springframework.core.io.Resource; @@ -73,7 +84,9 @@ public class ContestQueryService { private final ContestSortConvenience contestSortConvenience; private final TeamConvenience teamConvenience; private final TeamVoteConvenience teamVoteConvenience; + private final TeamLikeConvenience teamLikeConvenience; private final MemberConvenience memberConvenience; + private final ContestAwardConvenience contestAwardConvenience; private final FileConvenience fileConvenience; public ImageResponse getContestBanner(final Long contestId) { @@ -215,10 +228,64 @@ private static List applyDenseRanking(List getContestTeamSummaries(final Long contestId, final Member member) { + final Contest contest = contestConvenience.getValidateExistContest(contestId); + final List teams = teamConvenience.findAllByContestId(contestId); + + final boolean isVotingPeriod = checkVotingPeriod(contest); + + final ReactionMaps reactionMaps = getReactionMaps(contestId, member, isVotingPeriod); + final Map voteMap = reactionMaps.voteMap(); + final Map likeMap = reactionMaps.likeMap(); + + final List teamAwards = contestAwardConvenience.getTeamAwards(teams); + + teamConvenience.shuffleTeams(teams, member); + + return teams.stream() + .map(team -> TeamSummaryResponse.of( + team, + teamAwards, + likeMap.getOrDefault(team.getId(), false), + voteMap.getOrDefault(team.getId(), false) + )) + .toList(); + } + + public List getContestTeamSummariesPublic(final Long contestId) { + return getContestTeamSummaries(contestId, null); + } + + private ReactionMaps getReactionMaps(final Long contestId, final Member member, final boolean isVotingPeriod) { + if (member == null) { + return new ReactionMaps(Map.of(), Map.of()); + } + + if (isVotingPeriod) { + return new ReactionMaps( + teamVoteConvenience.getVoteMap(contestId, member), + Map.of() + ); + } else { + return new ReactionMaps( + Map.of(), + teamLikeConvenience.getLikeMap(contestId, member) + ); + } + } + + private boolean checkVotingPeriod(final Contest contest) { + final LocalDateTime now = LocalDateTime.now(); + return !now.isBefore(contest.getVoteStartAt()) + && !now.isAfter(contest.getVoteEndAt()); + } + private void checkImageConverted(final File findFile) { if (!findFile.getIsWebpConverted()) { throw new FileException(NOT_WEBP_CONVERTED); } } + private record ReactionMaps(Map voteMap, Map likeMap) { + } } diff --git a/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestAwardConvenience.java b/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestAwardConvenience.java index f2cf290..057ff45 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestAwardConvenience.java +++ b/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestAwardConvenience.java @@ -5,6 +5,10 @@ import com.opus.opus.modules.contest.domain.ContestAward; import com.opus.opus.modules.contest.domain.dao.ContestAwardRepository; import com.opus.opus.modules.contest.exception.ContestAwardException; +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.TeamContestAward; +import com.opus.opus.modules.team.domain.dao.TeamContestAwardRepository; +import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -14,6 +18,7 @@ public class ContestAwardConvenience { private final ContestAwardRepository contestAwardRepository; + private final TeamContestAwardRepository teamContestAwardRepository; public List findAllById(final List awardIds) { final List contestAwards = contestAwardRepository.findAllById(awardIds); @@ -24,4 +29,18 @@ public List findAllById(final List awardIds) { return contestAwards; } + + public List getTeamAwards(final List teams) { + final List teamIds = teams.stream().map(Team::getId).toList(); + + final List teamAwards = teamContestAwardRepository.findByTeamIdIn(teamIds); + + if (teamAwards.isEmpty()) { + return Collections.emptyList(); + } + + final List awardIds = teamAwards.stream().map(TeamContestAward::getContestAwardId).distinct().toList(); + + return findAllById(awardIds); + } } diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/response/TeamSummaryResponse.java b/src/main/java/com/opus/opus/modules/contest/application/dto/response/TeamSummaryResponse.java new file mode 100644 index 0000000..539d753 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/response/TeamSummaryResponse.java @@ -0,0 +1,46 @@ +package com.opus.opus.modules.contest.application.dto.response; + +import com.opus.opus.modules.contest.domain.ContestAward; +import com.opus.opus.modules.team.domain.Team; +import java.util.List; + +public record TeamSummaryResponse( + Long teamId, + String teamName, + String projectName, + Boolean isLiked, + Boolean isVoted, + List awards +) { + public static TeamSummaryResponse of( + final Team team, + final List contestAwards, + final Boolean isLiked, + final Boolean isVoted + ) { + final List awardInfos = contestAwards.stream() + .map(AwardInfo::from) + .toList(); + + return new TeamSummaryResponse( + team.getId(), + team.getTeamName(), + team.getProjectName(), + isLiked, + isVoted, + awardInfos + ); + } + + public record AwardInfo( + String awardName, + String awardColor + ) { + public static AwardInfo from(final ContestAward contestAward) { + return new AwardInfo( + contestAward.getAwardName(), + contestAward.getAwardColor() + ); + } + } +} diff --git a/src/main/java/com/opus/opus/modules/team/application/convenience/TeamContestAwardConvenience.java b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamContestAwardConvenience.java new file mode 100644 index 0000000..e5ecc6d --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamContestAwardConvenience.java @@ -0,0 +1,36 @@ +package com.opus.opus.modules.team.application.convenience; + +import com.opus.opus.modules.contest.application.convenience.ContestAwardConvenience; +import com.opus.opus.modules.contest.domain.ContestAward; +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.TeamContestAward; +import com.opus.opus.modules.team.domain.dao.TeamContestAwardRepository; +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TeamContestAwardConvenience { + + private final TeamContestAwardRepository teamContestAwardRepository; + + private final ContestAwardConvenience contestAwardConvenience; + + public List getTeamAwards(final List teams) { + final List teamIds = teams.stream().map(Team::getId).toList(); + + final List teamAwards = teamContestAwardRepository.findByTeamIdIn(teamIds); + + if (teamAwards.isEmpty()) { + return Collections.emptyList(); + } + + final List awardIds = teamAwards.stream().map(TeamContestAward::getContestAwardId).distinct().toList(); + + return contestAwardConvenience.findAllById(awardIds); + } +} diff --git a/src/main/java/com/opus/opus/modules/team/application/convenience/TeamConvenience.java b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamConvenience.java index 73dffc1..b02261d 100644 --- a/src/main/java/com/opus/opus/modules/team/application/convenience/TeamConvenience.java +++ b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamConvenience.java @@ -5,10 +5,13 @@ import static com.opus.opus.modules.team.exception.TeamExceptionType.NOT_FOUND_TEAM; import static com.opus.opus.modules.team.exception.TeamExceptionType.TRACK_HAS_TEAM; +import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.dao.TeamRepository; import com.opus.opus.modules.team.exception.TeamException; +import java.util.Collections; import java.util.List; +import java.util.Random; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,4 +46,18 @@ public void validateAllTeamsDeletedInTrack(final Long trackId) { public List getTeamsOfContest(final Long contestId) { return teamRepository.findAllByContestId(contestId); } + + public List findAllByContestId(final Long contestId) { + return teamRepository.findAllByContestId(contestId); + } + + public void shuffleTeams(final List teams, final Member member) { + if (member != null) { + Random seed = new Random(member.getId()); + Collections.shuffle(teams, seed); + } else { + Collections.shuffle(teams); + } + } + } diff --git a/src/main/java/com/opus/opus/modules/team/application/convenience/TeamLikeConvenience.java b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamLikeConvenience.java new file mode 100644 index 0000000..8b45c5f --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamLikeConvenience.java @@ -0,0 +1,26 @@ +package com.opus.opus.modules.team.application.convenience; + +import static java.util.stream.Collectors.toMap; + +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.modules.team.domain.TeamLike; +import com.opus.opus.modules.team.domain.dao.TeamLikeRepository; +import java.util.Collections; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TeamLikeConvenience { + + private final TeamLikeRepository teamLikeRepository; + + public Map getLikeMap(final Long contestId, final Member member) { + return (member != null) ? teamLikeRepository.findAllByMemberIdAndContestId(member.getId(), contestId).stream() + .collect(toMap(tl -> tl.getTeam().getId(), TeamLike::getIsLiked)) + : Collections.emptyMap(); + } +} diff --git a/src/main/java/com/opus/opus/modules/team/application/convenience/TeamVoteConvenience.java b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamVoteConvenience.java index f7af31d..8f33fa0 100644 --- a/src/main/java/com/opus/opus/modules/team/application/convenience/TeamVoteConvenience.java +++ b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamVoteConvenience.java @@ -1,8 +1,13 @@ package com.opus.opus.modules.team.application.convenience; +import static java.util.stream.Collectors.toMap; + +import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.domain.TeamVote; import com.opus.opus.modules.team.domain.dao.TeamVoteRepository; import java.util.List; +import java.util.Collections; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -19,4 +24,10 @@ public class TeamVoteConvenience { public Page getAllTeamVoteDesc(final List teamIds, final Pageable pageable) { return teamVoteRepository.findByTeamIdInOrderByCreatedAtDesc(teamIds, pageable); } + + public Map getVoteMap(final Long contestId, final Member member) { + return (member != null) ? teamVoteRepository.findAllByMemberIdAndContestId(member.getId(), contestId).stream() + .collect(toMap(tv -> tv.getTeam().getId(), TeamVote::getIsVoted)) + : Collections.emptyMap(); + } } diff --git a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamContestAwardRepository.java b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamContestAwardRepository.java index 4f76baa..6446501 100644 --- a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamContestAwardRepository.java +++ b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamContestAwardRepository.java @@ -9,4 +9,6 @@ public interface TeamContestAwardRepository extends JpaRepository { List findByTeamId(final Long teamId); + + List findByTeamIdIn(final List teamIds); } diff --git a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java index 0ea4a37..29213d3 100644 --- a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java +++ b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java @@ -2,10 +2,20 @@ import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamLike; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface TeamLikeRepository extends JpaRepository { Optional findByMemberIdAndTeam(Long memberId, Team team); + + @Query(""" + SELECT tl + FROM TeamLike tl + JOIN tl.team t + WHERE tl.memberId = :memberId AND t.contestId = :contestId + """) + List findAllByMemberIdAndContestId(Long memberId, Long contestId); } diff --git a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamVoteRepository.java b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamVoteRepository.java index 5f527b3..c649520 100644 --- a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamVoteRepository.java +++ b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamVoteRepository.java @@ -27,4 +27,12 @@ public interface TeamVoteRepository extends JpaRepository { "JOIN vote.team team " + "WHERE team.contestId = :contestId AND vote.isVoted = true") VoteStatisticsResult countVoteStatisticsByContest(Long contestId); + + @Query(""" + SELECT tv + FROM TeamVote tv + JOIN tv.team t + WHERE tv.memberId = :memberId AND t.contestId = :contestId + """) + List findAllByMemberIdAndContestId(final Long memberId, final Long contestId); } diff --git a/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java index 0fd28e1..eb73459 100644 --- a/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java +++ b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java @@ -12,24 +12,25 @@ import com.opus.opus.helper.IntegrationTest; import com.opus.opus.member.MemberFixture; import com.opus.opus.modules.contest.application.ContestQueryService; -import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSubmissionResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; import com.opus.opus.modules.contest.domain.Contest; +import com.opus.opus.modules.contest.domain.dao.ContestCategoryRepository; import com.opus.opus.modules.contest.domain.dao.ContestRepository; import com.opus.opus.modules.contest.exception.ContestException; import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.member.domain.dao.MemberRepository; +import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.dao.TeamRepository; import com.opus.opus.modules.team.domain.dao.TeamVoteRepository; -import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; import com.opus.opus.team.TeamFixture; import com.opus.opus.team.TeamVoteFixture; - import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -51,6 +52,8 @@ public class ContestQueryServiceTest extends IntegrationTest { private TeamRepository teamRepository; @Autowired private TeamVoteRepository teamVoteRepository; + @Autowired + private ContestCategoryRepository contestCategoryRepository; private Contest contest; private Team team; @@ -61,8 +64,8 @@ void setUp() { Contest newContest = ContestFixture.createContest(); newContest.updateVotePeriod(LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1)); newContest.updateMaxVotesLimit(2); - contest = contestRepository.save(newContest); + contest = contestRepository.save(newContest); team = teamRepository.save(TeamFixture.createTeamWithContestId(contest.getId())); member = memberRepository.save(MemberFixture.createMember()); } @@ -130,7 +133,7 @@ void setUp() { assertThat(contestVoteLogResponses.size()).isEqualTo(3); assertThat(contestVoteLogResponses.get(0).votedAt()).isAfterOrEqualTo(contestVoteLogResponses.get(1).votedAt()); } - + @Test @DisplayName("[성공] 사용자의 남은 투표 개수를 조회할 수 있다.") void 사용자의_남은_투표_개수를_조회할_수_있다() { @@ -266,7 +269,8 @@ void setUp() { void 팀이_없는_대회는_빈_리스트를_반환한다() { final Contest emptyContest = contestRepository.save(ContestFixture.createContest()); - final List responseList = contestQueryService.getTeamSubmissions(emptyContest.getId()); + final List responseList = contestQueryService.getTeamSubmissions( + emptyContest.getId()); assertThat(responseList).isEmpty(); } @@ -280,4 +284,19 @@ void setUp() { .isInstanceOf(ContestException.class) .hasMessage(NOT_FOUND_CONTEST.errorMessage()); } + + @Test + @DisplayName("[성공] 대회의 팀 목록을 조회할 수 있다.") + void 대회의_팀_목록을_조회할_수_있다() { + final Team team1 = teamRepository.save(TeamFixture.createTeamWithContestId(contest.getId())); + final Team team2 = teamRepository.save(TeamFixture.createTeamWithContestId(contest.getId())); + + final List responses = contestQueryService.getContestTeamSummaries(contest.getId(), + member); + + assertThat(responses).hasSize(3); // setup에서 저장된 team 포함 + assertThat(responses) + .extracting(TeamSummaryResponse::teamId) + .containsExactlyInAnyOrder(team.getId(), team1.getId(), team2.getId()); + } } diff --git a/src/test/java/com/opus/opus/restdocs/RestDocsTest.java b/src/test/java/com/opus/opus/restdocs/RestDocsTest.java index 0a8eb69..2cef42f 100644 --- a/src/test/java/com/opus/opus/restdocs/RestDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/RestDocsTest.java @@ -17,6 +17,9 @@ import com.opus.opus.modules.contest.application.ContestQueryService; import com.opus.opus.modules.contest.application.ContestTrackCommandService; import com.opus.opus.modules.contest.application.ContestTrackQueryService; +import com.opus.opus.modules.contest.api.ContestController; +import com.opus.opus.modules.contest.application.ContestCommandService; +import com.opus.opus.modules.contest.application.ContestQueryService; import com.opus.opus.modules.member.api.MemberController; import com.opus.opus.modules.member.application.MemberCommandService; import com.opus.opus.modules.member.application.MemberQueryService; diff --git a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java index 492aecb..b513a87 100644 --- a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java @@ -12,6 +12,7 @@ import static java.time.LocalDateTime.now; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.doNothing; @@ -32,9 +33,11 @@ import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.util.ReflectionTestUtils.setField; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.opus.opus.member.MemberFixture; import com.opus.opus.modules.contest.application.dto.request.ContestRequest; import com.opus.opus.modules.contest.application.dto.request.ContestSortCustomRequest; import com.opus.opus.modules.contest.application.dto.request.ContestSortRequest; @@ -42,15 +45,17 @@ import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import com.opus.opus.modules.contest.application.dto.response.ContestResponse; -import com.opus.opus.modules.contest.application.dto.response.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSortResponse; -import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSubmissionResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; import com.opus.opus.modules.contest.exception.ContestException; import com.opus.opus.modules.file.exception.FileException; import com.opus.opus.modules.file.exception.FileExceptionType; +import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.restdocs.RestDocsTest; import java.time.LocalDateTime; @@ -70,13 +75,17 @@ public class ContestApiDocsTest extends RestDocsTest { private static final String ADMIN_TOKEN = "Bearer admin.access.token"; + private static final String MEMBER_TOKEN = "Bearer member.access.token"; private String authorizationHeaderDescription; private byte[] testImage; + private Member member; @BeforeEach void setUp() { authorizationHeaderDescription = "Bearer %s.access.token"; testImage = "test-image-content".getBytes(); + member = MemberFixture.createMember(); + setField(member, "id", 1L); } @Test @@ -885,4 +894,135 @@ void setUp() { ) )); } + + @Test + @DisplayName("[성공] 비회원용 메인 페이지 API로 대회의 팀 목록을 조회한다.") + void 비회원용_대회의_팀_목록을_조회한다() throws Exception { + final List awards = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000"), + new TeamSummaryResponse.AwardInfo("우수상", "#00A3FF") + ); + + final List responses = List.of( + new TeamSummaryResponse(1L, "team1", "team1 Project", false, false, awards), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) + ); + + when(contestQueryService.getContestTeamSummariesPublic(anyLong())).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/teams/public", 1L)) + .andExpect(status().isOk()) + .andDo(document("get-contest-team-summaries-public", + pathParameters( + parameterWithName("contestId").description("대회의 고유 ID") + ), + responseFields( + arrayFieldWithPath("[]", "팀 목록"), + numberFieldWithPath("[].teamId", "팀 ID"), + stringFieldWithPath("[].teamName", "팀명"), + stringFieldWithPath("[].projectName", "프로젝트명"), + booleanFieldWithPath("[].isLiked", "좋아요 여부 (항상 false)"), + booleanFieldWithPath("[].isVoted", "투표 여부 (항상 false)"), + arrayFieldWithPath("[].awards", "수상 목록"), + stringFieldWithPath("[].awards[].awardName", "수상명"), + stringFieldWithPath("[].awards[].awardColor", "수상 색상") + ) + )); + } + + @Test + @DisplayName("[성공] 회원 전용 메인 페이지 API로 팀 목록을 조회한다. (미투표 기간)") + void 회원이_대회의_팀_목록을_조회한다_미투표_기간() throws Exception { + final List awards = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000") + ); + final List responses = List.of( + new TeamSummaryResponse(1L, "team1", "team1 Project", true, false, awards), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) + ); + + when(contestQueryService.getContestTeamSummaries(anyLong(), any())).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/teams", 1L) + .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) + .andExpect(status().isOk()) + .andDo(document("get-contest-team-summaries-member-non-voting", + pathParameters( + parameterWithName("contestId").description("대회의 고유 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "member")) + ), + responseFields( + arrayFieldWithPath("[]", "팀 목록"), + numberFieldWithPath("[].teamId", "팀 ID"), + stringFieldWithPath("[].teamName", "팀명"), + stringFieldWithPath("[].projectName", "프로젝트명"), + booleanFieldWithPath("[].isLiked", + "좋아요 여부 (투표 기간인 경우 false, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), + booleanFieldWithPath("[].isVoted", + "투표 여부 (투표 기간이 아닌 경우 false, 회원은 로그인한 사용자의 투표 여부에 따라)"), + arrayFieldWithPath("[].awards", "수상 목록"), + stringFieldWithPath("[].awards[].awardName", "수상명"), + stringFieldWithPath("[].awards[].awardColor", "수상 색상") + ) + )); + } + + @Test + @DisplayName("[성공] 회원 전용 메인 페이지 API로 팀 목록을 조회한다. (투표 기간)") + void 회원이_대회의_팀_목록을_조회한다_투표_기간() throws Exception { + final List awards = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000") + ); + final List responses = List.of( + new TeamSummaryResponse(1L, "team1", "team1 Project", false, true, awards), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) + ); + + when(contestQueryService.getContestTeamSummaries(anyLong(), any())).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/teams", 1L) + .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) + .andExpect(status().isOk()) + .andDo(document("get-contest-team-summaries-member-voting", + pathParameters( + parameterWithName("contestId").description("대회의 고유 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "member")) + ), + responseFields( + arrayFieldWithPath("[]", "팀 목록"), + numberFieldWithPath("[].teamId", "팀 ID"), + stringFieldWithPath("[].teamName", "팀명"), + stringFieldWithPath("[].projectName", "프로젝트명"), + booleanFieldWithPath("[].isLiked", + "좋아요 여부 (투표 기간인 경우 false, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), + booleanFieldWithPath("[].isVoted", + "투표 여부 (투표 기간이 아닌 경우 false, 회원은 로그인한 사용자의 투표 여부에 따라)"), + arrayFieldWithPath("[].awards", "수상 목록"), + stringFieldWithPath("[].awards[].awardName", "수상명"), + stringFieldWithPath("[].awards[].awardColor", "수상 색상") + ) + )); + } + + @Test + @DisplayName("[실패] 존재하지 않는 대회 ID로 조회 시 404 에러를 반환한다.") + void 존재하지_않는_대회_ID로_조회_시_에러를_반환한다() throws Exception { + willThrow(new ContestException(NOT_FOUND_CONTEST)) + .given(contestQueryService) + .getContestTeamSummaries(anyLong(), any()); + + mockMvc.perform(get("/contests/{contestId}/teams", 999L)) + .andExpect(status().isNotFound()) + .andDo(document("get-contest-team-summaries-fail-contest-not-found", + pathParameters( + parameterWithName("contestId").description("존재하지 않는 대회 ID") + ) + )); + } }