Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/main/java/com/opus/opus/docs/asciidoc/contest.adoc
Original file line number Diff line number Diff line change
@@ -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[]

====

6 changes: 6 additions & 0 deletions src/main/java/com/opus/opus/docs/asciidoc/opus.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ endif::[]

== 멤버 관련 API
link:./member.html[회원 API]

== 공지사항 관련 API
link:./notice.html[공지사항 API]

== 대회 관련 API
link:./contest.html[대회 API]
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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;
import com.opus.opus.modules.contest.application.dto.request.ContestRequest;
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;
Expand Down Expand Up @@ -101,4 +104,13 @@ public ResponseEntity<List<ContestCurrentResponse>> getCurrentContests() {
List<ContestCurrentResponse> responses = contestQueryService.getCurrentContests();
return ResponseEntity.ok(responses);
}

@GetMapping("/{contestId}/teams")
public ResponseEntity<List<TeamSummaryResponse>> getAllContestTeamSummaries(
@PathVariable final Long contestId,
@LoginMember final Member member
) {
final List<TeamSummaryResponse> responses = contestQueryService.getContestTeamSummaries(contestId, member);
return ResponseEntity.ok(responses);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@
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.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;
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.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;
Expand All @@ -35,7 +46,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);
Expand Down Expand Up @@ -72,6 +87,50 @@ public List<ContestResponse> getAllContests() {
.toList();
}

public List<TeamSummaryResponse> getContestTeamSummaries(final Long contestId, final Member member) {
final Contest contest = contestConvenience.getValidateExistContest(contestId);
final List<Team> teams = teamConvenience.findAllByContestId(contestId);

final boolean isVotingPeriod = checkVotingPeriod(contest);

final Pair<Map<Long, Boolean>, Map<Long, Boolean>> voteAndLikeMaps = getVoteAndLikeMaps(teams, member,
isVotingPeriod);
final Map<Long, Boolean> voteMap = voteAndLikeMaps.a;
final Map<Long, Boolean> likeMap = voteAndLikeMaps.b;

final List<ContestAward> 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<Long, Boolean>, Map<Long, Boolean>> getVoteAndLikeMaps(
final List<Team> teams, final Member member, final boolean isVotingPeriod) {
if (isVotingPeriod) {
return new Pair<>(
teamVoteConvenience.getVoteMap(teams, member),
Collections.emptyMap()
);
} else {
return new Pair<>(
Collections.emptyMap(),
teamLikeConvenience.getLikeMap(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +18,7 @@
public class ContestAwardConvenience {

private final ContestAwardRepository contestAwardRepository;
private final TeamContestAwardRepository teamContestAwardRepository;

public List<ContestAward> findAllById(final List<Long> awardIds) {
final List<ContestAward> contestAwards = contestAwardRepository.findAllById(awardIds);
Expand All @@ -24,4 +29,18 @@ public List<ContestAward> findAllById(final List<Long> awardIds) {

return contestAwards;
}

public List<ContestAward> getTeamAwards(final List<Team> teams) {
final List<Long> teamIds = teams.stream().map(Team::getId).toList();

final List<TeamContestAward> teamAwards = teamContestAwardRepository.findByTeamIdIn(teamIds);

if (teamAwards.isEmpty()) {
return Collections.emptyList();
}

final List<Long> awardIds = teamAwards.stream().map(TeamContestAward::getContestAwardId).distinct().toList();

return findAllById(awardIds);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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<AwardInfo> awards
) {
public static TeamSummaryResponse of(
final Team team,
final List<ContestAward> contestAwards,
final Boolean isLiked,
final Boolean isVoted,
final boolean isVotingPeriod
) {
final List<AwardInfo> 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()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ContestAward> getTeamAwards(final List<Team> teams) {
final List<Long> teamIds = teams.stream().map(Team::getId).toList();

final List<TeamContestAward> teamAwards = teamContestAwardRepository.findByTeamIdIn(teamIds);

if (teamAwards.isEmpty()) {
return Collections.emptyList();
}

final List<Long> awardIds = teamAwards.stream().map(TeamContestAward::getContestAwardId).distinct().toList();

return contestAwardConvenience.findAllById(awardIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,4 +43,17 @@ public void validateAllTeamsDeletedInTrack(final Long trackId) {
}
}

public List<Team> findAllByContestId(final Long contestId) {
return teamRepository.findByContestId(contestId);
}

public void shuffleTeams(final List<Team> teams, final Member member) {
if (member != null) {
Random seed = new Random(member.getId());
Collections.shuffle(teams, seed);
} else {
Collections.shuffle(teams);
}
}

}
Loading