From 51be080f22a4d418a0635e14f45b1dc5913ad4d1 Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 00:49:34 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat=20:=20=EB=8C=80=ED=9A=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20controller=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../opus/modules/contest/api/ContestController.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 3c0ebee6..5f3f1e25 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 @@ -1,5 +1,6 @@ package com.opus.opus.modules.contest.api; +import com.opus.opus.global.security.annotation.LoginMember; import com.opus.opus.modules.contest.application.ContestCommandService; import com.opus.opus.modules.contest.application.ContestQueryService; import com.opus.opus.modules.contest.application.dto.request.ContestCurrentToggleRequest; @@ -7,6 +8,8 @@ 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.ContestResponse; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; +import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.application.dto.ImageResponse; import jakarta.validation.Valid; import java.util.List; @@ -101,4 +104,13 @@ public ResponseEntity> getCurrentContests() { List responses = contestQueryService.getCurrentContests(); 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); + } } From 397452482160bf664da169d23e2b1e82b2bd343b Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 00:52:50 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat=20:=20TeamVote=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20schema.sql?= =?UTF-8?q?=EC=97=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../opus/modules/team/domain/TeamVote.java | 43 +++++++++++++++++++ src/main/resources/schema.sql | 11 +++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/com/opus/opus/modules/team/domain/TeamVote.java diff --git a/src/main/java/com/opus/opus/modules/team/domain/TeamVote.java b/src/main/java/com/opus/opus/modules/team/domain/TeamVote.java new file mode 100644 index 00000000..3c13057c --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/domain/TeamVote.java @@ -0,0 +1,43 @@ +package com.opus.opus.modules.team.domain; + +import static jakarta.persistence.FetchType.LAZY; + +import com.opus.opus.global.base.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TeamVote extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long memberId; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "team_id", nullable = false) + private Team team; + + @Column(nullable = false) + private Boolean isVoted; + + @Builder + public TeamVote(final Long memberId, final Team team, final Boolean isVoted) { + this.memberId = memberId; + this.team = team; + this.isVoted = isVoted; + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 6d06ffbd..56f919ed 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -13,6 +13,7 @@ DROP TABLE IF EXISTS `team_comment`; DROP TABLE IF EXISTS `team_contest_award`; DROP TABLE IF EXISTS `team_like`; DROP TABLE IF EXISTS `team_member`; +DROP TABLE IF EXISTS `team_vote`; DROP TABLE IF EXISTS `team_member_roles`; DROP TABLE IF EXISTS `team_sort`; @@ -154,6 +155,16 @@ CREATE TABLE `team_like` ( PRIMARY KEY (`id`) ); +CREATE TABLE `team_vote` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) DEFAULT NULL, + `updated_at` datetime(6) DEFAULT NULL, + `is_voted` bit(1) NOT NULL, + `member_id` bigint NOT NULL, + `team_id` bigint NOT NULL, + PRIMARY KEY (`id`) +); + CREATE TABLE `team_member` ( `id` bigint NOT NULL AUTO_INCREMENT, `created_at` datetime(6) DEFAULT NULL, From dc12844f247fbedef42498b3fd7242d6481e5b13 Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 00:56:24 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat=20:=20=EB=8C=80=ED=9A=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20Service=20?= =?UTF-8?q?=EB=B0=8F=20Response=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ContestQueryService.java | 31 ++++++++++ .../dto/response/TeamSummaryResponse.java | 59 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/main/java/com/opus/opus/modules/contest/application/dto/response/TeamSummaryResponse.java 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 941f0f8f..27d72646 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 @@ -6,17 +6,25 @@ import static com.opus.opus.modules.file.exception.FileExceptionType.NOT_WEBP_CONVERTED; 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.dto.response.ContestCurrentResponse; import com.opus.opus.modules.contest.application.dto.response.ContestResponse; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; import com.opus.opus.modules.contest.domain.Contest; import com.opus.opus.modules.contest.domain.ContestCategory; import com.opus.opus.modules.contest.domain.dao.ContestRepository; import com.opus.opus.modules.file.application.convenience.FileConvenience; import com.opus.opus.modules.file.domain.File; import com.opus.opus.modules.file.exception.FileException; +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 java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.antlr.v4.runtime.misc.Pair; @@ -35,7 +43,11 @@ public class ContestQueryService { private final ContestCategoryConvenience contestCategoryConvenience; private final ContestConvenience contestConvenience; + private final ContestAwardConvenience contestAwardConvenience; private final FileConvenience fileConvenience; + private final TeamConvenience teamConvenience; + private final TeamLikeConvenience teamLikeConvenience; + private final TeamVoteConvenience teamVoteConvenience; public ImageResponse getContestBanner(final Long contestId) { contestConvenience.getValidateExistContest(contestId); @@ -72,6 +84,25 @@ public List getAllContests() { .toList(); } + public 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); + + if (isVotingPeriod) { + return teamVoteConvenience.getAllTeamSummaries(teams, member); + } else { + return teamLikeConvenience.getAllTeamSummaries(teams, 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); 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 00000000..072fbea4 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/response/TeamSummaryResponse.java @@ -0,0 +1,59 @@ +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 boolean isVotingPeriod + ) { + final List awardInfos = contestAwards.stream() + .map(AwardInfo::from) + .toList(); + + if (isVotingPeriod) { + return new TeamSummaryResponse( + team.getId(), + team.getTeamName(), + team.getProjectName(), + null, + isVoted, + awardInfos + ); + } else { + return new TeamSummaryResponse( + team.getId(), + team.getTeamName(), + team.getProjectName(), + isLiked, + null, + awardInfos + ); + } + } + + public record AwardInfo( + String awardName, + String awardColor + ) { + public static AwardInfo from(final ContestAward contestAward) { + return new AwardInfo( + contestAward.getAwardName(), + contestAward.getAwardColor() + ); + } + } +} + From 335a575ad812d810f38b082d18dff76baf289241 Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 01:14:28 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat=20:=20=ED=88=AC=ED=91=9C=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?TeamLike=20=ED=98=B9=EC=9D=80=20TeamVote=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=20=EB=8C=80=ED=9A=8C=EC=9D=98=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=EC=9D=98=20=EC=9A=94=EC=95=BD=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../convenience/TeamConvenience.java | 17 ++++++++ .../convenience/TeamLikeConvenience.java | 42 +++++++++++++++++++ .../convenience/TeamVoteConvenience.java | 42 +++++++++++++++++++ .../dao/TeamContestAwardRepository.java | 2 + .../team/domain/dao/TeamLikeRepository.java | 12 ++++++ .../team/domain/dao/TeamRepository.java | 2 + .../team/domain/dao/TeamVoteRepository.java | 10 +++++ 7 files changed, 127 insertions(+) create mode 100644 src/main/java/com/opus/opus/modules/team/application/convenience/TeamLikeConvenience.java create mode 100644 src/main/java/com/opus/opus/modules/team/application/convenience/TeamVoteConvenience.java create mode 100644 src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java create mode 100644 src/main/java/com/opus/opus/modules/team/domain/dao/TeamVoteRepository.java 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 41c7583a..0dfb85d4 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,9 +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; @@ -39,4 +43,17 @@ public void validateAllTeamsDeletedInTrack(final Long trackId) { } } + public List findAllByContestId(final Long contestId) { + return teamRepository.findByContestId(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 00000000..34b2d76e --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamLikeConvenience.java @@ -0,0 +1,42 @@ +package com.opus.opus.modules.team.application.convenience; + +import static java.util.stream.Collectors.toMap; + +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; +import com.opus.opus.modules.contest.domain.ContestAward; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.TeamLike; +import com.opus.opus.modules.team.domain.dao.TeamLikeRepository; +import java.util.Collections; +import java.util.List; +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; + private final TeamContestAwardConvenience teamContestAwardConvenience; + private final TeamConvenience teamConvenience; + + public List getAllTeamSummaries(final List teams, final Member member) { + + teamConvenience.shuffleTeams(teams, member); + + final Map likeMap = + (member != null) ? teamLikeRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() + .collect(toMap(tl -> tl.getTeam().getId(), TeamLike::getIsLiked)) + : Collections.emptyMap(); + + final List teamAwards = teamContestAwardConvenience.getTeamAwards(teams); + + return teams.stream() + .map(team -> TeamSummaryResponse.of(team, teamAwards, likeMap.getOrDefault(team.getId(), false), + null, false)).toList(); + } +} 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 new file mode 100644 index 00000000..ba6bfa06 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/application/convenience/TeamVoteConvenience.java @@ -0,0 +1,42 @@ +package com.opus.opus.modules.team.application.convenience; + +import static java.util.stream.Collectors.toMap; + +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; +import com.opus.opus.modules.contest.domain.ContestAward; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.TeamVote; +import com.opus.opus.modules.team.domain.dao.TeamVoteRepository; +import java.util.Collections; +import java.util.List; +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 TeamVoteConvenience { + + private final TeamVoteRepository teamVoteRepository; + private final TeamContestAwardConvenience teamContestAwardConvenience; + private final TeamConvenience teamConvenience; + + public List getAllTeamSummaries(final List teams, final Member member) { + teamConvenience.shuffleTeams(teams, member); + + final Map voteMap = + (member != null) ? teamVoteRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() + .collect(toMap(tv -> tv.getTeam().getId(), TeamVote::getIsVoted)) + : Collections.emptyMap(); + + final List teamAwards = teamContestAwardConvenience.getTeamAwards(teams); + + return teams.stream() + .map(team -> TeamSummaryResponse.of(team, teamAwards, null, + voteMap.getOrDefault(team.getId(), false), + true)).toList(); + } +} 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 4f76baaa..6446501d 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 new file mode 100644 index 00000000..c493369c --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java @@ -0,0 +1,12 @@ +package com.opus.opus.modules.team.domain.dao; + +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.TeamLike; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TeamLikeRepository extends JpaRepository { + + List findAllByMemberIdAndTeamIn(final Long memberId, final List teams); + +} diff --git a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamRepository.java b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamRepository.java index efa5dd0f..31a458e8 100644 --- a/src/main/java/com/opus/opus/modules/team/domain/dao/TeamRepository.java +++ b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamRepository.java @@ -1,6 +1,7 @@ package com.opus.opus.modules.team.domain.dao; import com.opus.opus.modules.team.domain.Team; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -10,4 +11,5 @@ public interface TeamRepository extends JpaRepository { boolean existsByTrackId(final Long trackId); + List findByContestId(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 new file mode 100644 index 00000000..be816d52 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamVoteRepository.java @@ -0,0 +1,10 @@ +package com.opus.opus.modules.team.domain.dao; + +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.TeamVote; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TeamVoteRepository extends JpaRepository { + List findAllByMemberIdAndTeamIn(final Long memberId, final List teams); +} From bf4fbc00f58401a754cd9aca57bc2470bedcce8c Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 01:15:20 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat=20:=20=EA=B0=81=20=ED=8C=80=EC=9D=98?= =?UTF-8?q?=20=EC=88=98=EC=83=81=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TeamContestAwardConvenience.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/com/opus/opus/modules/team/application/convenience/TeamContestAwardConvenience.java 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 00000000..e5ecc6d3 --- /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); + } +} From e766c9dcaff477de0ba4182116c6fd3d2cc3bbbe Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 01:41:29 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat=20:=20=EC=A4=91=EB=B3=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ContestQueryService.java | 32 +++++++++++++++++-- .../convenience/ContestAwardConvenience.java | 19 +++++++++++ .../dto/response/TeamSummaryResponse.java | 1 - .../convenience/TeamLikeConvenience.java | 22 +++---------- .../convenience/TeamVoteConvenience.java | 22 +++---------- 5 files changed, 57 insertions(+), 39 deletions(-) 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 27d72646..b144ed7e 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 @@ -13,6 +13,7 @@ import com.opus.opus.modules.contest.application.dto.response.ContestResponse; import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; 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.dao.ContestRepository; import com.opus.opus.modules.file.application.convenience.FileConvenience; @@ -25,7 +26,9 @@ import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.modules.team.domain.Team; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.antlr.v4.runtime.misc.Pair; import org.springframework.core.io.Resource; @@ -90,10 +93,35 @@ public List getContestTeamSummaries(final Long contestId, f final boolean isVotingPeriod = checkVotingPeriod(contest); + final Pair, Map> voteAndLikeMaps = getVoteAndLikeMaps(teams, member, + isVotingPeriod); + final Map voteMap = voteAndLikeMaps.a; + final Map likeMap = voteAndLikeMaps.b; + + 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), + isVotingPeriod)).toList(); + + } + + private Pair, Map> getVoteAndLikeMaps( + final List teams, final Member member, final boolean isVotingPeriod) { if (isVotingPeriod) { - return teamVoteConvenience.getAllTeamSummaries(teams, member); + return new Pair<>( + teamVoteConvenience.getVoteMap(teams, member), + Collections.emptyMap() + ); } else { - return teamLikeConvenience.getAllTeamSummaries(teams, member); + return new Pair<>( + Collections.emptyMap(), + teamLikeConvenience.getLikeMap(teams, member) + ); } } 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 f2cf290b..057ff455 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 index 072fbea4..a976e2b1 100644 --- 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 @@ -56,4 +56,3 @@ public static AwardInfo from(final ContestAward contestAward) { } } } - 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 index 34b2d76e..00418984 100644 --- 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 @@ -2,8 +2,6 @@ import static java.util.stream.Collectors.toMap; -import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; -import com.opus.opus.modules.contest.domain.ContestAward; import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamLike; @@ -21,22 +19,10 @@ public class TeamLikeConvenience { private final TeamLikeRepository teamLikeRepository; - private final TeamContestAwardConvenience teamContestAwardConvenience; - private final TeamConvenience teamConvenience; - public List getAllTeamSummaries(final List teams, final Member member) { - - teamConvenience.shuffleTeams(teams, member); - - final Map likeMap = - (member != null) ? teamLikeRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() - .collect(toMap(tl -> tl.getTeam().getId(), TeamLike::getIsLiked)) - : Collections.emptyMap(); - - final List teamAwards = teamContestAwardConvenience.getTeamAwards(teams); - - return teams.stream() - .map(team -> TeamSummaryResponse.of(team, teamAwards, likeMap.getOrDefault(team.getId(), false), - null, false)).toList(); + public Map getLikeMap(final List teams, final Member member) { + return (member != null) ? teamLikeRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).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 ba6bfa06..6dc5f88c 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 @@ -2,8 +2,6 @@ import static java.util.stream.Collectors.toMap; -import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; -import com.opus.opus.modules.contest.domain.ContestAward; import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamVote; @@ -21,22 +19,10 @@ public class TeamVoteConvenience { private final TeamVoteRepository teamVoteRepository; - private final TeamContestAwardConvenience teamContestAwardConvenience; - private final TeamConvenience teamConvenience; - public List getAllTeamSummaries(final List teams, final Member member) { - teamConvenience.shuffleTeams(teams, member); - - final Map voteMap = - (member != null) ? teamVoteRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() - .collect(toMap(tv -> tv.getTeam().getId(), TeamVote::getIsVoted)) - : Collections.emptyMap(); - - final List teamAwards = teamContestAwardConvenience.getTeamAwards(teams); - - return teams.stream() - .map(team -> TeamSummaryResponse.of(team, teamAwards, null, - voteMap.getOrDefault(team.getId(), false), - true)).toList(); + public Map getVoteMap(final List teams, final Member member) { + return (member != null) ? teamVoteRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() + .collect(toMap(tv -> tv.getTeam().getId(), TeamVote::getIsVoted)) + : Collections.emptyMap(); } } From fdbb5ebc9b5c7aab9cce8e879f6707b8b85fa9ff Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 02:12:13 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat=20:=20ServiceTest=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../opus/contest/ContestCategoryFixture.java | 13 ++++ .../com/opus/opus/contest/ContestFixture.java | 14 ++++ .../application/ContestQueryServiceTest.java | 68 +++++++++++++++++++ .../java/com/opus/opus/team/TeamFixture.java | 25 +++++++ 4 files changed, 120 insertions(+) create mode 100644 src/test/java/com/opus/opus/contest/ContestCategoryFixture.java create mode 100644 src/test/java/com/opus/opus/contest/ContestFixture.java create mode 100644 src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java create mode 100644 src/test/java/com/opus/opus/team/TeamFixture.java diff --git a/src/test/java/com/opus/opus/contest/ContestCategoryFixture.java b/src/test/java/com/opus/opus/contest/ContestCategoryFixture.java new file mode 100644 index 00000000..aed7a46c --- /dev/null +++ b/src/test/java/com/opus/opus/contest/ContestCategoryFixture.java @@ -0,0 +1,13 @@ +package com.opus.opus.contest; + +import com.opus.opus.modules.contest.domain.ContestCategory; + +public class ContestCategoryFixture { + + public static ContestCategory createContestCategory() { + return ContestCategory.builder() + .categoryName("테스트 카테고리") + .build(); + } +} + diff --git a/src/test/java/com/opus/opus/contest/ContestFixture.java b/src/test/java/com/opus/opus/contest/ContestFixture.java new file mode 100644 index 00000000..37a85be3 --- /dev/null +++ b/src/test/java/com/opus/opus/contest/ContestFixture.java @@ -0,0 +1,14 @@ +package com.opus.opus.contest; + +import com.opus.opus.modules.contest.domain.Contest; + +public class ContestFixture { + + public static Contest createContest(final Long categoryId) { + return Contest.builder() + .contestName("테스트 대회") + .categoryId(categoryId) + .build(); + } +} + diff --git a/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java new file mode 100644 index 00000000..62553f2e --- /dev/null +++ b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java @@ -0,0 +1,68 @@ +package com.opus.opus.contest.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.opus.opus.contest.ContestCategoryFixture; +import com.opus.opus.contest.ContestFixture; +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.TeamSummaryResponse; +import com.opus.opus.modules.contest.domain.Contest; +import com.opus.opus.modules.contest.domain.ContestCategory; +import com.opus.opus.modules.contest.domain.dao.ContestCategoryRepository; +import com.opus.opus.modules.contest.domain.dao.ContestRepository; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.modules.member.domain.dao.MemberRepository; +import com.opus.opus.modules.team.domain.Team; +import com.opus.opus.modules.team.domain.dao.TeamRepository; +import com.opus.opus.team.TeamFixture; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ContestQueryServiceTest extends IntegrationTest { + + @Autowired + private ContestQueryService contestQueryService; + + @Autowired + private ContestRepository contestRepository; + + @Autowired + private ContestCategoryRepository contestCategoryRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private MemberRepository memberRepository; + + private ContestCategory category; + private Contest contest; + private Member member; + + @BeforeEach + void setUp() { + category = contestCategoryRepository.save(ContestCategoryFixture.createContestCategory()); + contest = contestRepository.save(ContestFixture.createContest(category.getId())); + member = memberRepository.save(MemberFixture.createMember()); + } + + @Test + @DisplayName("[성공] 대회의 팀 목록을 조회할 수 있다.") + void 대회의_팀_목록을_조회할_수_있다() { + final Team team1 = teamRepository.save(TeamFixture.createTeam(contest.getId())); + final Team team2 = teamRepository.save(TeamFixture.createTeam(contest.getId())); + + final List responses = contestQueryService.getContestTeamSummaries(contest.getId(), + member); + + assertThat(responses).hasSize(2); + assertThat(responses) + .extracting(TeamSummaryResponse::teamId) + .containsExactlyInAnyOrder(team1.getId(), team2.getId()); + } +} diff --git a/src/test/java/com/opus/opus/team/TeamFixture.java b/src/test/java/com/opus/opus/team/TeamFixture.java new file mode 100644 index 00000000..3808c4b3 --- /dev/null +++ b/src/test/java/com/opus/opus/team/TeamFixture.java @@ -0,0 +1,25 @@ +package com.opus.opus.team; + +import com.opus.opus.modules.team.domain.Team; + +public class TeamFixture { + + public static Team createTeam() { + return Team.builder() + .teamName("테스트 팀") + .projectName("테스트 프로젝트") + .contestId(1L) + .itemOrder(1) + .build(); + } + + public static Team createTeam(final Long contestId) { + return Team.builder() + .teamName("테스트 팀") + .projectName("테스트 프로젝트") + .contestId(contestId) + .itemOrder(1) + .build(); + } +} + From 2395df71f0f94fec6d2feac2269dc65deb68684f Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 02:33:42 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat=20:=20RestDocsTest=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/opus/opus/restdocs/RestDocsTest.java | 12 +- .../restdocs/docs/ContestApiDocsTest.java | 173 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java diff --git a/src/test/java/com/opus/opus/restdocs/RestDocsTest.java b/src/test/java/com/opus/opus/restdocs/RestDocsTest.java index 9452db4e..f45a0e0a 100644 --- a/src/test/java/com/opus/opus/restdocs/RestDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/RestDocsTest.java @@ -6,6 +6,9 @@ import com.opus.opus.global.security.JwtProvider; import com.opus.opus.helper.ApiTestHelper; +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; @@ -29,7 +32,8 @@ @WebMvcTest({ MemberController.class, - NoticeController.class + NoticeController.class, + ContestController.class }) @Import(RestDocsConfig.class) @ExtendWith(RestDocumentationExtension.class) @@ -48,6 +52,12 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockitoBean protected NoticeQueryService noticeQueryService; + @MockitoBean + protected ContestCommandService contestCommandService; + + @MockitoBean + protected ContestQueryService contestQueryService; + // Setting @Autowired protected WebApplicationContext context; diff --git a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java new file mode 100644 index 00000000..39d468d1 --- /dev/null +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java @@ -0,0 +1,173 @@ +package com.opus.opus.restdocs.docs; + +import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_FOUND_CONTEST; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.util.ReflectionTestUtils.setField; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.opus.opus.member.MemberFixture; +import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; +import com.opus.opus.modules.contest.exception.ContestException; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.restdocs.RestDocsTest; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.payload.JsonFieldType; + +public class ContestApiDocsTest extends RestDocsTest { + + private static final String MEMBER_TOKEN = "Bearer member.access.token"; + private Member member; + + @BeforeEach + void setUp() { + member = MemberFixture.createMember(); + setField(member, "id", 1L); + } + + @Test + @DisplayName("[성공] 비회원이 대회의 팀 목록을 조회할 수 있다.") + void 비회원이_대회의_팀_목록을_조회할_수_있다() throws Exception { + final List awards1 = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000"), + new TeamSummaryResponse.AwardInfo("우수상", "#00A3FF") + ); + final List awards2 = List.of(); + + final List responses = List.of( + new TeamSummaryResponse(1L, "team1", "team1 Project", false, null, awards1), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, null, awards2) + ); + + when(contestQueryService.getContestTeamSummaries(anyLong(), any())).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/teams", 1L)) + .andExpect(status().isOk()) + .andDo(document("get-contest-team-summaries", + pathParameters( + parameterWithName("contestId").description("대회의 고유 ID") + ), + responseFields( + arrayFieldWithPath("[]", "팀 목록"), + numberFieldWithPath("[].teamId", "팀 ID"), + stringFieldWithPath("[].teamName", "팀명"), + stringFieldWithPath("[].projectName", "프로젝트명"), + booleanFieldWithPath("[].isLiked", "좋아요 여부 (미투표 기간, 비회원은 항상 false)"), + fieldWithPath("[].isVoted").optional().type(JsonFieldType.BOOLEAN).description("투표 여부 (투표 기간인 경우, 미투표 기간에는 null)"), + arrayFieldWithPath("[].awards", "수상 목록"), + stringFieldWithPath("[].awards[].awardName", "수상명"), + stringFieldWithPath("[].awards[].awardColor", "수상 색상") + ) + )); + } + + @Test + @DisplayName("[성공] 회원이 대회의 팀 목록을 조회할 수 있다. (미투표 기간)") + void 회원이_대회의_팀_목록을_조회할_수_있다_미투표_기간() throws Exception { + final List awards1 = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000") + ); + final List awards2 = List.of(); + + final List responses = List.of( + new TeamSummaryResponse(1L, "team1", "team1 Project", true, null, awards1), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, null, awards2) + ); + + when(contestQueryService.getContestTeamSummaries(anyLong(), any(Member.class))).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/teams", 1L) + .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) + .andExpect(status().isOk()) + .andDo(document("get-contest-team-summaries-with-auth", + pathParameters( + parameterWithName("contestId").description("대회의 고유 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (선택)") + ), + responseFields( + arrayFieldWithPath("[]", "팀 목록"), + numberFieldWithPath("[].teamId", "팀 ID"), + stringFieldWithPath("[].teamName", "팀명"), + stringFieldWithPath("[].projectName", "프로젝트명"), + booleanFieldWithPath("[].isLiked", "좋아요 여부 (미투표 기간, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), + fieldWithPath("[].isVoted").optional().type(JsonFieldType.BOOLEAN).description("투표 여부 (투표 기간인 경우, 미투표 기간에는 null)"), + arrayFieldWithPath("[].awards", "수상 목록"), + stringFieldWithPath("[].awards[].awardName", "수상명"), + stringFieldWithPath("[].awards[].awardColor", "수상 색상") + ) + )); + } + + @Test + @DisplayName("[성공] 회원이 대회의 팀 목록을 조회할 수 있다. (투표 기간)") + void 회원이_대회의_팀_목록을_조회할_수_있다_투표_기간() throws Exception { + final List awards1 = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000"), + new TeamSummaryResponse.AwardInfo("우수상", "#00A3FF") + ); + final List awards2 = List.of(); + + final List responses = List.of( + new TeamSummaryResponse(1L, "team1", "team1 Project", null, true, awards1), + new TeamSummaryResponse(2L, "team2", "team2 Project", null, false, awards2) + ); + + when(contestQueryService.getContestTeamSummaries(anyLong(), any(Member.class))).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/teams", 1L) + .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) + .andExpect(status().isOk()) + .andDo(document("get-contest-team-summaries-with-auth-voting", + pathParameters( + parameterWithName("contestId").description("대회의 고유 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (선택)") + ), + responseFields( + arrayFieldWithPath("[]", "팀 목록"), + numberFieldWithPath("[].teamId", "팀 ID"), + stringFieldWithPath("[].teamName", "팀명"), + stringFieldWithPath("[].projectName", "프로젝트명"), + fieldWithPath("[].isLiked").optional().type(JsonFieldType.BOOLEAN).description("좋아요 여부 (미투표 기간인 경우, 투표 기간에는 null)"), + booleanFieldWithPath("[].isVoted", "투표 여부 (투표 기간인 경우, 회원은 로그인한 사용자의 투표 여부에 따라)"), + 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") + ) + )); + } +} From 55dfa897c1e3ad5b9ce899bccaf236f8fe569665 Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 1 Jan 2026 02:34:37 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat=20:=20adoc=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/opus/opus/docs/asciidoc/contest.adoc | 89 +++++++++++++++++++ .../com/opus/opus/docs/asciidoc/opus.adoc | 6 ++ 2 files changed, 95 insertions(+) create mode 100644 src/main/java/com/opus/opus/docs/asciidoc/contest.adoc diff --git a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc new file mode 100644 index 00000000..0ead0db4 --- /dev/null +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc @@ -0,0 +1,89 @@ +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + += CONTEST API 문서 +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 3 +:sectnums: + +== API 목록 + +link:../opus.html[API 목록으로 돌아가기] + +== `GET`: 대회의 팀 목록 조회 (비회원) + +NOTE: 비회원도 접근 가능한 메인 페이지용 API입니다. 현재 시간에 따라 응답의 필드에 `isVoted` 또는 `isLiked` 필드 중 하나만 포함됩니다. + +.HTTP Request +include::{snippets}/get-contest-team-summaries/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries/http-response.adoc[] + +.Path Parameters +include::{snippets}/get-contest-team-summaries/path-parameters.adoc[] + +.Response Body's Fields +include::{snippets}/get-contest-team-summaries/response-fields.adoc[] + +== `GET`: 대회의 팀 목록 조회 (회원 - 미투표 기간) + +NOTE: 회원이 조회할 수 있는 API입니다. 미투표 기간에는 `isLiked` 필드가 포함됩니다. + +.HTTP Request Headers +include::{snippets}/get-contest-team-summaries-with-auth/request-headers.adoc[] + +.HTTP Request +include::{snippets}/get-contest-team-summaries-with-auth/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries-with-auth/http-response.adoc[] + +.Path Parameters +include::{snippets}/get-contest-team-summaries-with-auth/path-parameters.adoc[] + +.Response Body's Fields +include::{snippets}/get-contest-team-summaries-with-auth/response-fields.adoc[] + +== `GET`: 대회의 팀 목록 조회 (회원 - 투표 기간) + +NOTE: 회원이 조회할 수 있는 API입니다. 투표 기간에는 `isVoted` 필드가 포함됩니다. + +.HTTP Request Headers +include::{snippets}/get-contest-team-summaries-with-auth-voting/request-headers.adoc[] + +.HTTP Request +include::{snippets}/get-contest-team-summaries-with-auth-voting/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-team-summaries-with-auth-voting/http-response.adoc[] + +.Path Parameters +include::{snippets}/get-contest-team-summaries-with-auth-voting/path-parameters.adoc[] + +.Response Body's Fields +include::{snippets}/get-contest-team-summaries-with-auth-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/docs/asciidoc/opus.adoc b/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc index 2a1b6c46..57c727de 100644 --- a/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc +++ b/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc @@ -11,3 +11,9 @@ endif::[] == 멤버 관련 API link:./member.html[회원 API] + +== 공지사항 관련 API +link:./notice.html[공지사항 API] + +== 대회 관련 API +link:./contest.html[대회 API] From 7708a5bfab315b456c867f7495f8fc7b56619b10 Mon Sep 17 00:00:00 2001 From: myeowon Date: Tue, 24 Feb 2026 03:46:35 +0900 Subject: [PATCH 10/16] =?UTF-8?q?feat=20:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9A=A9=20=EC=A0=84=EC=B2=B4=20=ED=8C=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20=ED=9A=8C=EC=9B=90=EA=B3=BC=20?= =?UTF-8?q?=EB=B9=84=ED=9A=8C=EC=9B=90=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contest/api/ContestController.java | 13 +++++++--- .../application/ContestQueryService.java | 25 ++++++++++++++++--- .../convenience/TeamConvenience.java | 6 ++++- 3 files changed, 37 insertions(+), 7 deletions(-) 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 0f8cf43b..c44d2673 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,18 +11,17 @@ 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.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.TeamQueryService; -import com.opus.opus.modules.contest.application.dto.response.TeamSummaryResponse; -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 java.util.List; import lombok.RequiredArgsConstructor; @@ -36,6 +35,7 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -198,4 +198,11 @@ public ResponseEntity> getAllContestTeamSummaries( 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 a7383374..653392d6 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 @@ -6,16 +6,15 @@ import static com.opus.opus.modules.file.exception.FileExceptionType.NOT_WEBP_CONVERTED; 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; import com.opus.opus.modules.contest.application.dto.response.ContestCurrentResponse; 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.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; @@ -128,8 +127,28 @@ public List getContestTeamSummaries(final Long contestId, f .map(team -> TeamSummaryResponse.of(team, teamAwards, likeMap.getOrDefault(team.getId(), false), voteMap.getOrDefault(team.getId(), false), - isVotingPeriod)).toList(); + isVotingPeriod + )) + .toList(); + } + public List getContestTeamSummariesPublic(final Long contestId) { + final Contest contest = contestConvenience.getValidateExistContest(contestId); + final List teams = teamConvenience.findAllByContestId(contestId); + + final boolean isVotingPeriod = checkVotingPeriod(contest); + + final List teamAwards = contestAwardConvenience.getTeamAwards(teams); + + teamConvenience.shuffleTeams(teams); + + return teams.stream() + .map(team -> TeamSummaryResponse.of(team, teamAwards, + false, + false, + isVotingPeriod + )) + .toList(); } private Pair, Map> getVoteAndLikeMaps( 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 7ea08ff4..ee945ab9 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 @@ -9,8 +9,8 @@ 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.List; import java.util.Collections; +import java.util.List; import java.util.Random; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -60,4 +60,8 @@ public void shuffleTeams(final List teams, final Member member) { } } + public void shuffleTeams(final List teams) { + Collections.shuffle(teams); + } + } From 118addcaa146fe53b4fd5684890df4a475b9e438 Mon Sep 17 00:00:00 2001 From: myeowon Date: Wed, 25 Feb 2026 00:03:21 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor=20:=20=ED=9A=8C=EC=9B=90/?= =?UTF-8?q?=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=8C=80=ED=9A=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ContestQueryService.java | 53 ++++++++----------- .../convenience/TeamConvenience.java | 6 +-- .../convenience/TeamLikeConvenience.java | 6 +-- .../convenience/TeamVoteConvenience.java | 6 +-- .../team/domain/dao/TeamLikeRepository.java | 10 ++++ .../team/domain/dao/TeamVoteRepository.java | 9 ++++ 6 files changed, 47 insertions(+), 43 deletions(-) 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 653392d6..7821aff6 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 @@ -6,6 +6,7 @@ import static com.opus.opus.modules.file.exception.FileExceptionType.NOT_WEBP_CONVERTED; 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; @@ -30,7 +31,6 @@ import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.modules.team.domain.Team; import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -51,6 +51,7 @@ public class ContestQueryService { private final ContestCategoryConvenience contestCategoryConvenience; private final ContestConvenience contestConvenience; private final ContestSortConvenience contestSortConvenience; + private final ContestAwardConvenience contestAwardConvenience; private final FileConvenience fileConvenience; private final TeamConvenience teamConvenience; private final TeamLikeConvenience teamLikeConvenience; @@ -114,17 +115,18 @@ public List getContestTeamSummaries(final Long contestId, f final boolean isVotingPeriod = checkVotingPeriod(contest); - final Pair, Map> voteAndLikeMaps = getVoteAndLikeMaps(teams, member, - isVotingPeriod); - final Map voteMap = voteAndLikeMaps.a; - final Map likeMap = voteAndLikeMaps.b; + 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, + .map(team -> TeamSummaryResponse.of( + team, + teamAwards, likeMap.getOrDefault(team.getId(), false), voteMap.getOrDefault(team.getId(), false), isVotingPeriod @@ -133,35 +135,23 @@ public List getContestTeamSummaries(final Long contestId, f } public List getContestTeamSummariesPublic(final Long contestId) { - final Contest contest = contestConvenience.getValidateExistContest(contestId); - final List teams = teamConvenience.findAllByContestId(contestId); - - final boolean isVotingPeriod = checkVotingPeriod(contest); - - final List teamAwards = contestAwardConvenience.getTeamAwards(teams); - - teamConvenience.shuffleTeams(teams); - - return teams.stream() - .map(team -> TeamSummaryResponse.of(team, teamAwards, - false, - false, - isVotingPeriod - )) - .toList(); + return getContestTeamSummaries(contestId, null); } - private Pair, Map> getVoteAndLikeMaps( - final List teams, final Member member, final boolean isVotingPeriod) { + 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 Pair<>( - teamVoteConvenience.getVoteMap(teams, member), - Collections.emptyMap() + return new ReactionMaps( + teamVoteConvenience.getVoteMap(contestId, member), + Map.of() ); } else { - return new Pair<>( - Collections.emptyMap(), - teamLikeConvenience.getLikeMap(teams, member) + return new ReactionMaps( + Map.of(), + teamLikeConvenience.getLikeMap(contestId, member) ); } } @@ -177,4 +167,7 @@ private void checkImageConverted(final File findFile) { throw new FileException(NOT_WEBP_CONVERTED); } } + + private record ReactionMaps(Map voteMap, Map likeMap) { + } } 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 ee945ab9..b02261d2 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 @@ -48,7 +48,7 @@ public List getTeamsOfContest(final Long contestId) { } public List findAllByContestId(final Long contestId) { - return teamRepository.findByContestId(contestId); + return teamRepository.findAllByContestId(contestId); } public void shuffleTeams(final List teams, final Member member) { @@ -60,8 +60,4 @@ public void shuffleTeams(final List teams, final Member member) { } } - public void shuffleTeams(final List teams) { - 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 index 00418984..8b45c5f5 100644 --- 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 @@ -3,11 +3,9 @@ import static java.util.stream.Collectors.toMap; import com.opus.opus.modules.member.domain.Member; -import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamLike; import com.opus.opus.modules.team.domain.dao.TeamLikeRepository; import java.util.Collections; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,8 +18,8 @@ public class TeamLikeConvenience { private final TeamLikeRepository teamLikeRepository; - public Map getLikeMap(final List teams, final Member member) { - return (member != null) ? teamLikeRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() + 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 6dc5f88c..1f389023 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 @@ -3,11 +3,9 @@ import static java.util.stream.Collectors.toMap; import com.opus.opus.modules.member.domain.Member; -import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamVote; import com.opus.opus.modules.team.domain.dao.TeamVoteRepository; import java.util.Collections; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,8 +18,8 @@ public class TeamVoteConvenience { private final TeamVoteRepository teamVoteRepository; - public Map getVoteMap(final List teams, final Member member) { - return (member != null) ? teamVoteRepository.findAllByMemberIdAndTeamIn(member.getId(), teams).stream() + 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/TeamLikeRepository.java b/src/main/java/com/opus/opus/modules/team/domain/dao/TeamLikeRepository.java index 0ea4a37e..29213d3f 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 b49aba61..dff1f27c 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 @@ -2,6 +2,7 @@ import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamVote; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -20,4 +21,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); } From f29d248cf3e07ecebb255b13a097e0a91f0d572c Mon Sep 17 00:00:00 2001 From: myeowon Date: Wed, 25 Feb 2026 00:42:39 +0900 Subject: [PATCH 12/16] =?UTF-8?q?chore=20:=20schema.sql=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 060cd4a3..868c7998 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -14,7 +14,6 @@ DROP TABLE IF EXISTS `team_comment`; DROP TABLE IF EXISTS `team_contest_award`; DROP TABLE IF EXISTS `team_like`; DROP TABLE IF EXISTS `team_member`; -DROP TABLE IF EXISTS `team_vote`; DROP TABLE IF EXISTS `team_member_roles`; DROP TABLE IF EXISTS `team_vote`; @@ -167,16 +166,6 @@ CREATE TABLE `team_like` ( UNIQUE KEY `uk_team_like_member_team` (`member_id`, `team_id`) ); -CREATE TABLE `team_vote` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `is_voted` bit(1) NOT NULL, - `member_id` bigint NOT NULL, - `team_id` bigint NOT NULL, - PRIMARY KEY (`id`) -); - CREATE TABLE `team_member` ( `id` bigint NOT NULL AUTO_INCREMENT, `created_at` datetime(6) DEFAULT NULL, From c5f33964f427f5c47aaedb812065699ca697b74c Mon Sep 17 00:00:00 2001 From: myeowon Date: Wed, 25 Feb 2026 01:01:59 +0900 Subject: [PATCH 13/16] =?UTF-8?q?feat=20:=20=EB=8C=80=ED=9A=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20serviceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contest/application/ContestQueryServiceTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 13a69dea..c78d0e86 100644 --- a/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java +++ b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java @@ -10,19 +10,19 @@ import com.opus.opus.member.MemberFixture; import com.opus.opus.modules.contest.application.ContestQueryService; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; -import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; 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.ContestCategory; 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 java.time.LocalDateTime; import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.member.domain.dao.MemberRepository; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.dao.TeamRepository; import com.opus.opus.team.TeamFixture; +import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -53,7 +53,7 @@ public class ContestQueryServiceTest extends IntegrationTest { @BeforeEach void setUp() { category = contestCategoryRepository.save(ContestCategoryFixture.createContestCategory()); - contest = contestRepository.save(ContestFixture.createContest(category.getId())); + contest = contestRepository.save(ContestFixture.createContestWithCategoryId(category.getId())); member = memberRepository.save(MemberFixture.createMember()); } @@ -102,8 +102,8 @@ void setUp() { @Test @DisplayName("[성공] 대회의 팀 목록을 조회할 수 있다.") void 대회의_팀_목록을_조회할_수_있다() { - final Team team1 = teamRepository.save(TeamFixture.createTeam(contest.getId())); - final Team team2 = teamRepository.save(TeamFixture.createTeam(contest.getId())); + 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); From 7d4abec86affa90dba4b418dfac41786554f5e3d Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 26 Feb 2026 21:15:59 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat=20:=20=EB=8C=80=ED=9A=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20api=20Test=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ContestQueryService.java | 3 +- .../dto/response/TeamSummaryResponse.java | 30 ++---- .../restdocs/docs/ContestApiDocsTest.java | 93 +++++++++---------- 3 files changed, 55 insertions(+), 71 deletions(-) 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 7821aff6..ac0fa535 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 @@ -128,8 +128,7 @@ public List getContestTeamSummaries(final Long contestId, f team, teamAwards, likeMap.getOrDefault(team.getId(), false), - voteMap.getOrDefault(team.getId(), false), - isVotingPeriod + voteMap.getOrDefault(team.getId(), false) )) .toList(); } 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 index a976e2b1..539d753a 100644 --- 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 @@ -16,32 +16,20 @@ public static TeamSummaryResponse of( final Team team, final List contestAwards, final Boolean isLiked, - final Boolean isVoted, - final boolean isVotingPeriod + final Boolean isVoted ) { final List awardInfos = contestAwards.stream() .map(AwardInfo::from) .toList(); - if (isVotingPeriod) { - return new TeamSummaryResponse( - team.getId(), - team.getTeamName(), - team.getProjectName(), - null, - isVoted, - awardInfos - ); - } else { - return new TeamSummaryResponse( - team.getId(), - team.getTeamName(), - team.getProjectName(), - isLiked, - null, - awardInfos - ); - } + return new TeamSummaryResponse( + team.getId(), + team.getTeamName(), + team.getProjectName(), + isLiked, + isVoted, + awardInfos + ); } public record AwardInfo( 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 8d298895..5b85d201 100644 --- a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java @@ -26,16 +26,16 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; 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.web.servlet.result.MockMvcResultMatchers.content; 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; @@ -43,17 +43,16 @@ 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.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; -import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; -import com.opus.opus.member.MemberFixture; 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.team.application.dto.ImageResponse; 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; import java.util.List; @@ -64,14 +63,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.restdocs.payload.JsonFieldType; 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 static final String MEMBER_TOKEN = "Bearer member.access.token"; private Member member; @BeforeEach @@ -795,24 +793,23 @@ void setUp() { } @Test - @DisplayName("[성공] 비회원이 대회의 팀 목록을 조회할 수 있다.") - void 비회원이_대회의_팀_목록을_조회할_수_있다() throws Exception { - final List awards1 = List.of( + @DisplayName("[성공] 비회원용 메인 페이지 API로 대회의 팀 목록을 조회한다.") + void 비회원용_대회의_팀_목록을_조회한다() throws Exception { + final List awards = List.of( new TeamSummaryResponse.AwardInfo("대상", "#FF0000"), new TeamSummaryResponse.AwardInfo("우수상", "#00A3FF") ); - final List awards2 = List.of(); final List responses = List.of( - new TeamSummaryResponse(1L, "team1", "team1 Project", false, null, awards1), - new TeamSummaryResponse(2L, "team2", "team2 Project", false, null, awards2) + new TeamSummaryResponse(1L, "team1", "team1 Project", false, false, awards), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) ); - when(contestQueryService.getContestTeamSummaries(anyLong(), any())).thenReturn(responses); + given(contestQueryService.getContestTeamSummariesPublic(anyLong())).willReturn(responses); - mockMvc.perform(get("/contests/{contestId}/teams", 1L)) + mockMvc.perform(get("/contests/{contestId}/teams/public", 1L)) .andExpect(status().isOk()) - .andDo(document("get-contest-team-summaries", + .andDo(document("get-contest-team-summaries-public", pathParameters( parameterWithName("contestId").description("대회의 고유 ID") ), @@ -821,9 +818,9 @@ void setUp() { numberFieldWithPath("[].teamId", "팀 ID"), stringFieldWithPath("[].teamName", "팀명"), stringFieldWithPath("[].projectName", "프로젝트명"), - booleanFieldWithPath("[].isLiked", "좋아요 여부 (미투표 기간, 비회원은 항상 false)"), - fieldWithPath("[].isVoted").optional().type(JsonFieldType.BOOLEAN).description("투표 여부 (투표 기간인 경우, 미투표 기간에는 null)"), - arrayFieldWithPath("[].awards", "수상 목록"), + booleanFieldWithPath("[].isLiked", "좋아요 여부 (항상 false)"), + booleanFieldWithPath("[].isVoted", "투표 여부 (항상 false)"), + arrayFieldWithPath("[].awards", "awards"), stringFieldWithPath("[].awards[].awardName", "수상명"), stringFieldWithPath("[].awards[].awardColor", "수상 색상") ) @@ -831,38 +828,39 @@ void setUp() { } @Test - @DisplayName("[성공] 회원이 대회의 팀 목록을 조회할 수 있다. (미투표 기간)") - void 회원이_대회의_팀_목록을_조회할_수_있다_미투표_기간() throws Exception { - final List awards1 = List.of( + @DisplayName("[성공] 회원 전용 메인 페이지 API로 팀 목록을 조회한다. (미투표 기간)") + void 회원이_대회의_팀_목록을_조회한다_미투표_기간() throws Exception { + final List awards = List.of( new TeamSummaryResponse.AwardInfo("대상", "#FF0000") ); - final List awards2 = List.of(); - final List responses = List.of( - new TeamSummaryResponse(1L, "team1", "team1 Project", true, null, awards1), - new TeamSummaryResponse(2L, "team2", "team2 Project", false, null, awards2) + new TeamSummaryResponse(1L, "team1", "team1 Project", true, false, awards), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) ); - when(contestQueryService.getContestTeamSummaries(anyLong(), any(Member.class))).thenReturn(responses); + given(contestQueryService.getContestTeamSummaries(anyLong(), any())).willReturn(responses); mockMvc.perform(get("/contests/{contestId}/teams", 1L) .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) .andExpect(status().isOk()) - .andDo(document("get-contest-team-summaries-with-auth", + .andDo(document("get-contest-team-summaries-member-non-voting", pathParameters( parameterWithName("contestId").description("대회의 고유 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (선택)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + "Authorization: Bearer ${ACCESS_TOKEN} 인증 방식, 액세스 토큰으로 인증 요청 (선택)") ), responseFields( arrayFieldWithPath("[]", "팀 목록"), numberFieldWithPath("[].teamId", "팀 ID"), stringFieldWithPath("[].teamName", "팀명"), stringFieldWithPath("[].projectName", "프로젝트명"), - booleanFieldWithPath("[].isLiked", "좋아요 여부 (미투표 기간, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), - fieldWithPath("[].isVoted").optional().type(JsonFieldType.BOOLEAN).description("투표 여부 (투표 기간인 경우, 미투표 기간에는 null)"), - arrayFieldWithPath("[].awards", "수상 목록"), + booleanFieldWithPath("[].isLiked", + "좋아요 여부 (투표 기간인 경우 false, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), + booleanFieldWithPath("[].isVoted", + "투표 여부 (투표 기간이 아닌 경우 false, 회원은 로그인한 사용자의 투표 여부에 따라)"), + arrayFieldWithPath("[].awards", "awards"), stringFieldWithPath("[].awards[].awardName", "수상명"), stringFieldWithPath("[].awards[].awardColor", "수상 색상") ) @@ -870,39 +868,38 @@ void setUp() { } @Test - @DisplayName("[성공] 회원이 대회의 팀 목록을 조회할 수 있다. (투표 기간)") - void 회원이_대회의_팀_목록을_조회할_수_있다_투표_기간() throws Exception { - final List awards1 = List.of( - new TeamSummaryResponse.AwardInfo("대상", "#FF0000"), - new TeamSummaryResponse.AwardInfo("우수상", "#00A3FF") + @DisplayName("[성공] 회원 전용 메인 페이지 API로 팀 목록을 조회한다. (투표 기간)") + void 회원이_대회의_팀_목록을_조회한다_투표_기간() throws Exception { + final List awards = List.of( + new TeamSummaryResponse.AwardInfo("대상", "#FF0000") ); - final List awards2 = List.of(); - final List responses = List.of( - new TeamSummaryResponse(1L, "team1", "team1 Project", null, true, awards1), - new TeamSummaryResponse(2L, "team2", "team2 Project", null, false, awards2) + new TeamSummaryResponse(1L, "team1", "team1 Project", false, true, awards), + new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) ); - when(contestQueryService.getContestTeamSummaries(anyLong(), any(Member.class))).thenReturn(responses); + given(contestQueryService.getContestTeamSummaries(anyLong(), any())).willReturn(responses); mockMvc.perform(get("/contests/{contestId}/teams", 1L) .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) .andExpect(status().isOk()) - .andDo(document("get-contest-team-summaries-with-auth-voting", + .andDo(document("get-contest-team-summaries-member-voting", pathParameters( parameterWithName("contestId").description("대회의 고유 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (선택)") + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken}") ), responseFields( arrayFieldWithPath("[]", "팀 목록"), numberFieldWithPath("[].teamId", "팀 ID"), stringFieldWithPath("[].teamName", "팀명"), stringFieldWithPath("[].projectName", "프로젝트명"), - fieldWithPath("[].isLiked").optional().type(JsonFieldType.BOOLEAN).description("좋아요 여부 (미투표 기간인 경우, 투표 기간에는 null)"), - booleanFieldWithPath("[].isVoted", "투표 여부 (투표 기간인 경우, 회원은 로그인한 사용자의 투표 여부에 따라)"), - arrayFieldWithPath("[].awards", "수상 목록"), + booleanFieldWithPath("[].isLiked", + "좋아요 여부 (투표 기간인 경우 false, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), + booleanFieldWithPath("[].isVoted", + "투표 여부 (투표 기간이 아닌 경우 false, 회원은 로그인한 사용자의 투표 여부에 따라)"), + arrayFieldWithPath("[].awards", "awards"), stringFieldWithPath("[].awards[].awardName", "수상명"), stringFieldWithPath("[].awards[].awardColor", "수상 색상") ) From b2b9c3de8c834516888233f7e6015743fd830282 Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 26 Feb 2026 23:07:53 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix=20:=20setUp=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contest/application/ContestQueryServiceTest.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 df6915c8..eb73459b 100644 --- a/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java +++ b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java @@ -8,7 +8,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.opus.opus.contest.ContestCategoryFixture; import com.opus.opus.contest.ContestFixture; import com.opus.opus.helper.IntegrationTest; import com.opus.opus.member.MemberFixture; @@ -21,7 +20,6 @@ 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.ContestCategory; 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; @@ -57,7 +55,6 @@ public class ContestQueryServiceTest extends IntegrationTest { @Autowired private ContestCategoryRepository contestCategoryRepository; - private ContestCategory category; private Contest contest; private Team team; private Member member; @@ -67,13 +64,10 @@ void setUp() { Contest newContest = ContestFixture.createContest(); newContest.updateVotePeriod(LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1)); newContest.updateMaxVotesLimit(2); - // TODO : category to newContest - category = contestCategoryRepository.save(ContestCategoryFixture.createContestCategory()); contest = contestRepository.save(newContest); team = teamRepository.save(TeamFixture.createTeamWithContestId(contest.getId())); member = memberRepository.save(MemberFixture.createMember()); - } @Test @@ -300,9 +294,9 @@ void setUp() { final List responses = contestQueryService.getContestTeamSummaries(contest.getId(), member); - assertThat(responses).hasSize(2); + assertThat(responses).hasSize(3); // setup에서 저장된 team 포함 assertThat(responses) .extracting(TeamSummaryResponse::teamId) - .containsExactlyInAnyOrder(team1.getId(), team2.getId()); + .containsExactlyInAnyOrder(team.getId(), team1.getId(), team2.getId()); } } From 936d144981313edc56ce41190678a89a905e0b94 Mon Sep 17 00:00:00 2001 From: myeowon Date: Thu, 26 Feb 2026 23:27:39 +0900 Subject: [PATCH 16/16] =?UTF-8?q?feat=20:=20=EB=8C=80=ED=9A=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20doc=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/opus/opus/docs/asciidoc/contest.adoc | 63 +++++++++---------- .../restdocs/docs/ContestApiDocsTest.java | 19 +++--- 2 files changed, 41 insertions(+), 41 deletions(-) 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 c046e5e4..acb09776 100644 --- a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc @@ -491,68 +491,68 @@ include::{snippets}/get-vote-statistics-fail-not-found/http-response.adoc[] ==== -== `GET`: 대회의 팀 목록 조회 (비회원) +== 대회 전체 팀 조회 -NOTE: 비회원도 접근 가능한 메인 페이지용 API입니다. 현재 시간에 따라 응답의 필드에 `isVoted` 또는 `isLiked` 필드 중 하나만 포함됩니다. +=== `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/http-request.adoc[] +include::{snippets}/get-contest-team-summaries-public/http-request.adoc[] .HTTP Response -include::{snippets}/get-contest-team-summaries/http-response.adoc[] - -.Path Parameters -include::{snippets}/get-contest-team-summaries/path-parameters.adoc[] +include::{snippets}/get-contest-team-summaries-public/http-response.adoc[] .Response Body's Fields -include::{snippets}/get-contest-team-summaries/response-fields.adoc[] +include::{snippets}/get-contest-team-summaries-public/response-fields.adoc[] -== `GET`: 대회의 팀 목록 조회 (회원 - 미투표 기간) +=== `GET`: 대회의 팀 목록 조회 (회원용 - 미투표 기간) -NOTE: 회원이 조회할 수 있는 API입니다. 미투표 기간에는 `isLiked` 필드가 포함됩니다. +NOTE: 회원만 접근 가능한 메인 페이지용 API입니다. 미투표 기간에는 로그인한 사용자의 `isLiked` 여부가 포함됩니다. -.HTTP Request Headers -include::{snippets}/get-contest-team-summaries-with-auth/request-headers.adoc[] +.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-with-auth/http-request.adoc[] +include::{snippets}/get-contest-team-summaries-member-non-voting/http-request.adoc[] .HTTP Response -include::{snippets}/get-contest-team-summaries-with-auth/http-response.adoc[] - -.Path Parameters -include::{snippets}/get-contest-team-summaries-with-auth/path-parameters.adoc[] +include::{snippets}/get-contest-team-summaries-member-non-voting/http-response.adoc[] .Response Body's Fields -include::{snippets}/get-contest-team-summaries-with-auth/response-fields.adoc[] +include::{snippets}/get-contest-team-summaries-member-non-voting/response-fields.adoc[] -== `GET`: 대회의 팀 목록 조회 (회원 - 투표 기간) +=== `GET`: 대회의 팀 목록 조회 (회원용 - 투표 기간) -NOTE: 회원이 조회할 수 있는 API입니다. 투표 기간에는 `isVoted` 필드가 포함됩니다. +NOTE: 회원만 접근 가능한 메인 페이지용 API입니다. 투표 기간에는 로그인한 사용자의 `isVoted` 여부가 포함됩니다. -.HTTP Request Headers -include::{snippets}/get-contest-team-summaries-with-auth-voting/request-headers.adoc[] +.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-with-auth-voting/http-request.adoc[] +include::{snippets}/get-contest-team-summaries-member-voting/http-request.adoc[] .HTTP Response -include::{snippets}/get-contest-team-summaries-with-auth-voting/http-response.adoc[] - -.Path Parameters -include::{snippets}/get-contest-team-summaries-with-auth-voting/path-parameters.adoc[] +include::{snippets}/get-contest-team-summaries-member-voting/http-response.adoc[] .Response Body's Fields -include::{snippets}/get-contest-team-summaries-with-auth-voting/response-fields.adoc[] +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[] @@ -561,5 +561,4 @@ include::{snippets}/get-contest-team-summaries-fail-contest-not-found/http-respo .Path Parameters include::{snippets}/get-contest-team-summaries-fail-contest-not-found/path-parameters.adoc[] - ==== 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 d2c54d9f..b513a87e 100644 --- a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java @@ -46,8 +46,8 @@ 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.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; @@ -908,7 +908,7 @@ void setUp() { new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) ); - given(contestQueryService.getContestTeamSummariesPublic(anyLong())).willReturn(responses); + when(contestQueryService.getContestTeamSummariesPublic(anyLong())).thenReturn(responses); mockMvc.perform(get("/contests/{contestId}/teams/public", 1L)) .andExpect(status().isOk()) @@ -923,7 +923,7 @@ void setUp() { stringFieldWithPath("[].projectName", "프로젝트명"), booleanFieldWithPath("[].isLiked", "좋아요 여부 (항상 false)"), booleanFieldWithPath("[].isVoted", "투표 여부 (항상 false)"), - arrayFieldWithPath("[].awards", "awards"), + arrayFieldWithPath("[].awards", "수상 목록"), stringFieldWithPath("[].awards[].awardName", "수상명"), stringFieldWithPath("[].awards[].awardColor", "수상 색상") ) @@ -941,7 +941,7 @@ void setUp() { new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) ); - given(contestQueryService.getContestTeamSummaries(anyLong(), any())).willReturn(responses); + when(contestQueryService.getContestTeamSummaries(anyLong(), any())).thenReturn(responses); mockMvc.perform(get("/contests/{contestId}/teams", 1L) .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) @@ -952,7 +952,7 @@ void setUp() { ), requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION).description( - "Authorization: Bearer ${ACCESS_TOKEN} 인증 방식, 액세스 토큰으로 인증 요청 (선택)") + String.format(authorizationHeaderDescription, "member")) ), responseFields( arrayFieldWithPath("[]", "팀 목록"), @@ -963,7 +963,7 @@ void setUp() { "좋아요 여부 (투표 기간인 경우 false, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), booleanFieldWithPath("[].isVoted", "투표 여부 (투표 기간이 아닌 경우 false, 회원은 로그인한 사용자의 투표 여부에 따라)"), - arrayFieldWithPath("[].awards", "awards"), + arrayFieldWithPath("[].awards", "수상 목록"), stringFieldWithPath("[].awards[].awardName", "수상명"), stringFieldWithPath("[].awards[].awardColor", "수상 색상") ) @@ -981,7 +981,7 @@ void setUp() { new TeamSummaryResponse(2L, "team2", "team2 Project", false, false, List.of()) ); - given(contestQueryService.getContestTeamSummaries(anyLong(), any())).willReturn(responses); + when(contestQueryService.getContestTeamSummaries(anyLong(), any())).thenReturn(responses); mockMvc.perform(get("/contests/{contestId}/teams", 1L) .header(HttpHeaders.AUTHORIZATION, MEMBER_TOKEN)) @@ -991,7 +991,8 @@ void setUp() { parameterWithName("contestId").description("대회의 고유 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken}") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "member")) ), responseFields( arrayFieldWithPath("[]", "팀 목록"), @@ -1002,7 +1003,7 @@ void setUp() { "좋아요 여부 (투표 기간인 경우 false, 회원은 로그인한 사용자의 좋아요 여부에 따라)"), booleanFieldWithPath("[].isVoted", "투표 여부 (투표 기간이 아닌 경우 false, 회원은 로그인한 사용자의 투표 여부에 따라)"), - arrayFieldWithPath("[].awards", "awards"), + arrayFieldWithPath("[].awards", "수상 목록"), stringFieldWithPath("[].awards[].awardName", "수상명"), stringFieldWithPath("[].awards[].awardColor", "수상 색상") )