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 bb18132a..9cd443e7 100644 --- a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc @@ -490,3 +490,58 @@ include::{snippets}/get-vote-statistics-fail-not-found/http-request.adoc[] include::{snippets}/get-vote-statistics-fail-not-found/http-response.adoc[] ==== + +== 대회 팀 템플릿 설정 관리 + +=== `GET`: 대회의 템플릿 조회 + +.Path Parameters +include::{snippets}/get-contest-template/path-parameters.adoc[] + +.HTTP Request +include::{snippets}/get-contest-template/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-contest-template/http-response.adoc[] + +.Response Fields +include::{snippets}/get-contest-template/response-fields.adoc[] + +==== ⚠️ 실패 케이스 + +.❌ Case 1: 존재하지 않는 대회 + +[%collapsible] +==== +include::{snippets}/get-contest-template-fail-not-found/path-parameters.adoc[] +include::{snippets}/get-contest-template-fail-not-found/http-request.adoc[] +include::{snippets}/get-contest-template-fail-not-found/http-response.adoc[] +==== + +=== `PUT`: 대회의 템플릿 수정 + +.Path Parameters +include::{snippets}/update-contest-template/path-parameters.adoc[] + +.Request Headers +include::{snippets}/update-contest-template/request-headers.adoc[] + +.Request Fields +include::{snippets}/update-contest-template/request-fields.adoc[] + +.HTTP Request +include::{snippets}/update-contest-template/http-request.adoc[] + +.HTTP Response +include::{snippets}/update-contest-template/http-response.adoc[] + +==== ⚠️ 실패 케이스 + +.❌ Case 1: 존재하지 않는 대회 + +[%collapsible] +==== +include::{snippets}/update-contest-template-fail-not-found/path-parameters.adoc[] +include::{snippets}/update-contest-template-fail-not-found/http-request.adoc[] +include::{snippets}/update-contest-template-fail-not-found/http-response.adoc[] +==== diff --git a/src/main/java/com/opus/opus/modules/contest/api/ContestController.java b/src/main/java/com/opus/opus/modules/contest/api/ContestController.java index 2e43f522..39a7346e 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 @@ -7,21 +7,23 @@ 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; +import com.opus.opus.modules.contest.application.dto.request.ContestTemplateRequest; import com.opus.opus.modules.contest.application.dto.request.ContestVotesLimitRequest; import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; import com.opus.opus.modules.contest.application.dto.response.ContestCurrentResponse; import com.opus.opus.modules.contest.application.dto.response.ContestCurrentToggleResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import com.opus.opus.modules.contest.application.dto.response.ContestResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSortResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSubmissionResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestTemplateResponse; 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.modules.member.domain.Member; import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; -import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.PositiveOrZero; import java.util.List; @@ -84,8 +86,8 @@ public ResponseEntity> getAllContests() { return ResponseEntity.ok(responses); } - @PostMapping @Secured("ROLE_관리자") + @PostMapping public ResponseEntity createContest(@Valid @RequestBody final ContestRequest request) { ContestResponse response = contestCommandService.createContest(request); return ResponseEntity.ok(response); @@ -205,4 +207,19 @@ public ResponseEntity> getTeamSubmissions(@PathV final List responses = contestQueryService.getTeamSubmissions(contestId); return ResponseEntity.ok(responses); } + + @GetMapping("/{contestId}/template") + public ResponseEntity getContestTemplate(@PathVariable final Long contestId) { + final ContestTemplateResponse response = contestQueryService.getContestTemplate(contestId); + return ResponseEntity.ok(response); + } + + + @Secured("ROLE_관리자") + @PutMapping("/{contestId}/template") + public ResponseEntity updateContestTemplate(@PathVariable final Long contestId, + @Valid @RequestBody final ContestTemplateRequest request) { + contestCommandService.updateContestTemplate(contestId, request); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/opus/opus/modules/contest/application/ContestCommandService.java b/src/main/java/com/opus/opus/modules/contest/application/ContestCommandService.java index 42b66b86..a858128c 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/ContestCommandService.java +++ b/src/main/java/com/opus/opus/modules/contest/application/ContestCommandService.java @@ -23,26 +23,30 @@ 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.convenience.ContestTemplateConvenience; 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; +import com.opus.opus.modules.contest.application.dto.request.ContestTemplateRequest; import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; 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.domain.Contest; import com.opus.opus.modules.contest.domain.ContestCategory; import com.opus.opus.modules.contest.domain.ContestSort; +import com.opus.opus.modules.contest.domain.ContestTemplate; import com.opus.opus.modules.contest.domain.dao.ContestRepository; import com.opus.opus.modules.contest.domain.dao.ContestSortRepository; +import com.opus.opus.modules.contest.domain.dao.ContestTemplateRepository; import com.opus.opus.modules.contest.exception.ContestException; import com.opus.opus.modules.file.domain.File; import com.opus.opus.modules.file.domain.dao.FileRepository; import com.opus.opus.modules.file.exception.FileException; import com.opus.opus.modules.team.application.convenience.TeamConvenience; -import java.util.Optional; import com.opus.opus.modules.team.domain.Team; import java.util.List; import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -58,19 +62,21 @@ public class ContestCommandService { private final ContestRepository contestRepository; private final ContestSortRepository contestSortRepository; private final FileRepository fileRepository; + private final ContestTemplateRepository contestTemplateRepository; private final ContestConvenience contestConvenience; private final ContestCategoryConvenience contestCategoryConvenience; private final ContestSortConvenience contestSortConvenience; private final TeamConvenience teamConvenience; + private final ContestTemplateConvenience contestTemplateConvenience; private final FileStorageUtil fileStorageUtil; - public void saveBannerImage(final Long contestId, final MultipartFile image) { contestConvenience.getValidateExistContest(contestId); - final Optional existingFile = fileRepository.findByReferenceIdAndReferenceTypeAndImageType(contestId, CONTEST, BANNER); + final Optional existingFile = fileRepository.findByReferenceIdAndReferenceTypeAndImageType(contestId, + CONTEST, BANNER); existingFile.ifPresent(this::checkWebpConverted); fileStorageUtil.storeFile(image, contestId, CONTEST, BANNER); @@ -99,6 +105,9 @@ public ContestResponse createContest(final ContestRequest request) { .contest(contest) .build()); + // 템플릿 자동 생성 + createTemplate(contest, contestCategory.getCategoryName()); + return ContestResponse.from(contest, contestCategory.getCategoryName()); } @@ -242,4 +251,61 @@ private void applyCustomSortToTeams(final List request team.updateItemOrder(r.itemOrder()); } } + + public void createTemplate(final Contest contest, final String categoryName) { + final ContestTemplateRequest request = getDefaultTemplateRequest(categoryName); + + final ContestTemplate template = ContestTemplate.builder() + .contest(contest) + .request(request) + .build(); + + contestTemplateRepository.save(template); + } + + public void updateContestTemplate(final Long contestId, final ContestTemplateRequest request) { + contestConvenience.validateExistContest(contestId); + final ContestTemplate template = contestTemplateConvenience.getValidateExistTemplate(contestId); + template.updateTemplate(request); + } + + private ContestTemplateRequest getDefaultTemplateRequest(final String categoryName) { + if (categoryName != null && categoryName.contains("창의융합")) { + return new ContestTemplateRequest( + true, + true, + true, + true, + true, + false, + true, + false, + false, + true, + true, + true + ); + } + + if (categoryName != null && categoryName.contains("캡스톤")) { + return new ContestTemplateRequest( + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + false, + true + ); + } + + return new ContestTemplateRequest( + false, false, false, false, false, false, false, false, false, false, false, false + ); + } } 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 fd22f994..2cee194b 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 @@ -10,18 +10,21 @@ 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.convenience.ContestTemplateConvenience; import com.opus.opus.modules.contest.application.dto.response.ContestCurrentResponse; 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.ContestTemplateResponse; +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.VotePeriodResponse; 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.domain.Contest; import com.opus.opus.modules.contest.domain.ContestCategory; import com.opus.opus.modules.contest.domain.ContestSort; +import com.opus.opus.modules.contest.domain.ContestTemplate; import com.opus.opus.modules.contest.domain.ContestTrack; import com.opus.opus.modules.contest.domain.dao.ContestRepository; import com.opus.opus.modules.contest.domain.dao.ContestTrackRepository; @@ -33,19 +36,17 @@ import com.opus.opus.modules.team.application.convenience.TeamConvenience; import com.opus.opus.modules.team.application.convenience.TeamVoteConvenience; import com.opus.opus.modules.team.application.dto.ImageResponse; +import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.TeamVote; -import java.util.List; -import java.util.Map; -import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; import com.opus.opus.modules.team.domain.dao.TeamRankingResult; import com.opus.opus.modules.team.domain.dao.TeamRepository; import com.opus.opus.modules.team.domain.dao.TeamVoteRepository; import com.opus.opus.modules.team.domain.dao.VoteStatisticsResult; import java.util.ArrayList; - +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; - import lombok.RequiredArgsConstructor; import org.antlr.v4.runtime.misc.Pair; import org.springframework.core.io.Resource; @@ -71,11 +72,31 @@ public class ContestQueryService { private final ContestCategoryConvenience contestCategoryConvenience; private final ContestConvenience contestConvenience; private final ContestSortConvenience contestSortConvenience; + private final ContestTemplateConvenience contestTemplateConvenience; private final TeamConvenience teamConvenience; private final TeamVoteConvenience teamVoteConvenience; private final MemberConvenience memberConvenience; private final FileConvenience fileConvenience; + private static List applyDenseRanking(List votesPerTeam) { + List responseList = new ArrayList<>(); + int curRank = 0; // 현재 순위 + long prevCount = -1; // 이전 팀 투표 수 + for (TeamRankingResult result : votesPerTeam) { + // 이전 팀과 투표 수가 다르면 순위 증가, 같으면 순위 유지 + if (prevCount != result.voteCount()) { + curRank++; + } + prevCount = result.voteCount(); + + responseList.add( + new ContestRankingResponse(curRank, result.teamId(), result.teamName(), result.projectName(), + result.trackName(), result.voteCount())); + } + + return responseList; + } + public ImageResponse getContestBanner(final Long contestId) { contestConvenience.getValidateExistContest(contestId); final File findBanner = fileConvenience.findByReferenceIdAndReferenceTypeAndImageType(contestId, CONTEST, @@ -200,25 +221,16 @@ public List getTeamSubmissions(Long contestId) { .toList(); } - private static List applyDenseRanking(List votesPerTeam) { - List responseList = new ArrayList<>(); - int curRank = 0; // 현재 순위 - long prevCount = -1; // 이전 팀 투표 수 - for (TeamRankingResult result : votesPerTeam) { - // 이전 팀과 투표 수가 다르면 순위 증가, 같으면 순위 유지 - if (prevCount != result.voteCount()) curRank++; - prevCount = result.voteCount(); - - responseList.add(new ContestRankingResponse(curRank, result.teamId(), result.teamName(), result.projectName(), result.trackName(), result.voteCount())); - } - - return responseList; - } - private void checkImageConverted(final File findFile) { if (!findFile.getIsWebpConverted()) { throw new FileException(NOT_WEBP_CONVERTED); } } + + public ContestTemplateResponse getContestTemplate(final Long contestId) { + contestConvenience.getValidateExistContest(contestId); + final ContestTemplate template = contestTemplateConvenience.getValidateExistTemplate(contestId); + return ContestTemplateResponse.from(template); + } } diff --git a/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestConvenience.java b/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestConvenience.java index 37413f71..c62a65c4 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestConvenience.java +++ b/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestConvenience.java @@ -1,12 +1,12 @@ package com.opus.opus.modules.contest.application.convenience; -import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_ALLOWED_DURING_VOTING_PERIOD; import static com.opus.opus.modules.contest.exception.ContestExceptionType.CATEGORY_HAS_CONTEST; import static com.opus.opus.modules.contest.exception.ContestExceptionType.CONTEST_NAME_ALREADY_EXIST; +import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_ALLOWED_DURING_VOTING_PERIOD; import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_FOUND_CONTEST; -import static org.springframework.transaction.annotation.Propagation.MANDATORY; import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_VOTE_PERIOD_NOW; +import static org.springframework.transaction.annotation.Propagation.MANDATORY; import com.opus.opus.modules.contest.domain.Contest; import com.opus.opus.modules.contest.domain.dao.ContestRepository; @@ -50,7 +50,7 @@ public long countCurrentContests() { public List getCurrentContests() { return contestRepository.findAllByIsCurrentTrue(); } - + @Transactional(propagation = MANDATORY) public Contest getValidateExistContestForUpdate(final Long contestId) { return contestRepository.findByIdForUpdate(contestId) diff --git a/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestTemplateConvenience.java b/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestTemplateConvenience.java new file mode 100644 index 00000000..24cfceb6 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/application/convenience/ContestTemplateConvenience.java @@ -0,0 +1,23 @@ +package com.opus.opus.modules.contest.application.convenience; + +import static com.opus.opus.modules.contest.exception.ContestTemplateExceptionType.NOT_FOUND_TEMPLATE; + +import com.opus.opus.modules.contest.domain.ContestTemplate; +import com.opus.opus.modules.contest.domain.dao.ContestTemplateRepository; +import com.opus.opus.modules.contest.exception.ContestTemplateException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ContestTemplateConvenience { + + private final ContestTemplateRepository contestTemplateRepository; + + public ContestTemplate getValidateExistTemplate(final Long contestId) { + return contestTemplateRepository.findByContestId(contestId) + .orElseThrow(() -> new ContestTemplateException(NOT_FOUND_TEMPLATE)); + } +} diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTemplateRequest.java b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTemplateRequest.java new file mode 100644 index 00000000..1c5698c4 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTemplateRequest.java @@ -0,0 +1,31 @@ +package com.opus.opus.modules.contest.application.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record ContestTemplateRequest( + @NotNull(message = "분과 필수 여부는 필수입니다.") + Boolean trackRequired, + @NotNull(message = "프로젝트명 필수 여부는 필수입니다.") + Boolean projectNameRequired, + @NotNull(message = "팀명 필수 여부는 필수입니다.") + Boolean teamNameRequired, + @NotNull(message = "팀장 필수 여부는 필수입니다.") + Boolean leaderRequired, + @NotNull(message = "팀원 필수 여부는 필수입니다.") + Boolean teamMembersRequired, + @NotNull(message = "지도 교수 필수 여부는 필수입니다.") + Boolean professorRequired, + @NotNull(message = "GitHub 링크 필수 여부는 필수입니다.") + Boolean githubPathRequired, + @NotNull(message = "YouTube 링크 필수 여부는 필수입니다.") + Boolean youTubePathRequired, + @NotNull(message = "배포 링크 필수 여부는 필수입니다.") + Boolean productionPathRequired, + @NotNull(message = "프로젝트 개요 필수 여부는 필수입니다.") + Boolean overviewRequired, + @NotNull(message = "포스터 필수 여부는 필수입니다.") + Boolean posterRequired, + @NotNull(message = "이미지 필수 여부는 필수입니다.") + Boolean imagesRequired +) { +} diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestTemplateResponse.java b/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestTemplateResponse.java new file mode 100644 index 00000000..cd2551bb --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestTemplateResponse.java @@ -0,0 +1,35 @@ +package com.opus.opus.modules.contest.application.dto.response; + +import com.opus.opus.modules.contest.domain.ContestTemplate; + +public record ContestTemplateResponse( + Boolean trackRequired, + Boolean projectNameRequired, + Boolean teamNameRequired, + Boolean leaderRequired, + Boolean teamMembersRequired, + Boolean professorRequired, + Boolean githubPathRequired, + Boolean youTubePathRequired, + Boolean productionPathRequired, + Boolean overviewRequired, + Boolean posterRequired, + Boolean imagesRequired +) { + public static ContestTemplateResponse from(final ContestTemplate template) { + return new ContestTemplateResponse( + template.getTrackRequired(), + template.getProjectNameRequired(), + template.getTeamNameRequired(), + template.getLeaderRequired(), + template.getTeamMembersRequired(), + template.getProfessorRequired(), + template.getGithubPathRequired(), + template.getYouTubePathRequired(), + template.getProductionPathRequired(), + template.getOverviewRequired(), + template.getPosterRequired(), + template.getImagesRequired() + ); + } +} diff --git a/src/main/java/com/opus/opus/modules/contest/domain/ContestTemplate.java b/src/main/java/com/opus/opus/modules/contest/domain/ContestTemplate.java new file mode 100644 index 00000000..1d9e5d18 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/domain/ContestTemplate.java @@ -0,0 +1,110 @@ +package com.opus.opus.modules.contest.domain; + +import static jakarta.persistence.FetchType.LAZY; + +import com.opus.opus.global.base.BaseEntity; +import com.opus.opus.modules.contest.application.dto.request.ContestTemplateRequest; +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.OneToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") +@SQLDelete(sql = "UPDATE contest_template SET is_deleted = true WHERE id = ?") +public class ContestTemplate extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "contest_id", nullable = false) + private Contest contest; + + @Column(nullable = false) + private Boolean trackRequired; + + @Column(nullable = false) + private Boolean projectNameRequired; + + @Column(nullable = false) + private Boolean teamNameRequired; + + @Column(nullable = false) + private Boolean leaderRequired; + + @Column(nullable = false) + private Boolean teamMembersRequired; + + @Column(nullable = false) + private Boolean professorRequired; + + @Column(nullable = false) + private Boolean githubPathRequired; + + @Column(nullable = false) + private Boolean youTubePathRequired; + + @Column(nullable = false) + private Boolean productionPathRequired; + + @Column(nullable = false) + private Boolean overviewRequired; + + @Column(nullable = false) + private Boolean posterRequired; + + @Column(nullable = false) + private Boolean imagesRequired; + + @Column(nullable = false) + private Boolean isDeleted; + + @Builder + private ContestTemplate(final Contest contest, final ContestTemplateRequest request + ) { + this.contest = contest; + + this.trackRequired = request.trackRequired(); + this.projectNameRequired = request.projectNameRequired(); + this.teamNameRequired = request.teamNameRequired(); + this.leaderRequired = request.leaderRequired(); + this.teamMembersRequired = request.teamMembersRequired(); + this.professorRequired = request.professorRequired(); + this.githubPathRequired = request.githubPathRequired(); + this.youTubePathRequired = request.youTubePathRequired(); + this.productionPathRequired = request.productionPathRequired(); + this.overviewRequired = request.overviewRequired(); + this.posterRequired = request.posterRequired(); + this.imagesRequired = request.imagesRequired(); + + this.isDeleted = false; + } + + public void updateTemplate(final ContestTemplateRequest request) { + this.trackRequired = request.trackRequired(); + this.projectNameRequired = request.projectNameRequired(); + this.teamNameRequired = request.teamNameRequired(); + this.leaderRequired = request.leaderRequired(); + this.teamMembersRequired = request.teamMembersRequired(); + this.professorRequired = request.professorRequired(); + this.githubPathRequired = request.githubPathRequired(); + this.youTubePathRequired = request.youTubePathRequired(); + this.productionPathRequired = request.productionPathRequired(); + this.overviewRequired = request.overviewRequired(); + this.posterRequired = request.posterRequired(); + this.imagesRequired = request.imagesRequired(); + } +} diff --git a/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestTemplateRepository.java b/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestTemplateRepository.java new file mode 100644 index 00000000..4dff8131 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestTemplateRepository.java @@ -0,0 +1,11 @@ +package com.opus.opus.modules.contest.domain.dao; + +import com.opus.opus.modules.contest.domain.ContestTemplate; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ContestTemplateRepository extends JpaRepository { + + Optional findByContestId(final Long contestId); + +} diff --git a/src/main/java/com/opus/opus/modules/contest/exception/ContestTemplateException.java b/src/main/java/com/opus/opus/modules/contest/exception/ContestTemplateException.java new file mode 100644 index 00000000..d583ebe3 --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/exception/ContestTemplateException.java @@ -0,0 +1,23 @@ +package com.opus.opus.modules.contest.exception; + +import com.opus.opus.global.base.BaseException; +import com.opus.opus.global.base.BaseExceptionType; + +public class ContestTemplateException extends BaseException { + private final ContestTemplateExceptionType exceptionType; + + public ContestTemplateException(final ContestTemplateExceptionType exceptionType) { + super(exceptionType.errorMessage()); + this.exceptionType = exceptionType; + } + + public ContestTemplateException(final ContestTemplateExceptionType exceptionType, final String message) { + super(message); + this.exceptionType = exceptionType; + } + + @Override + public BaseExceptionType exceptionType() { + return exceptionType; + } +} diff --git a/src/main/java/com/opus/opus/modules/contest/exception/ContestTemplateExceptionType.java b/src/main/java/com/opus/opus/modules/contest/exception/ContestTemplateExceptionType.java new file mode 100644 index 00000000..7ee8c9ee --- /dev/null +++ b/src/main/java/com/opus/opus/modules/contest/exception/ContestTemplateExceptionType.java @@ -0,0 +1,27 @@ +package com.opus.opus.modules.contest.exception; + +import com.opus.opus.global.base.BaseExceptionType; +import org.springframework.http.HttpStatus; + +public enum ContestTemplateExceptionType implements BaseExceptionType { + NOT_FOUND_TEMPLATE(HttpStatus.NOT_FOUND, "템플릿을 찾을 수 없습니다."), + ; + + private final HttpStatus httpStatus; + private final String errorMessage; + + ContestTemplateExceptionType(final HttpStatus httpStatus, final String errorMessage) { + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String errorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/com/opus/opus/modules/member/domain/Member.java b/src/main/java/com/opus/opus/modules/member/domain/Member.java index f060d2ea..274d5ca5 100644 --- a/src/main/java/com/opus/opus/modules/member/domain/Member.java +++ b/src/main/java/com/opus/opus/modules/member/domain/Member.java @@ -1,7 +1,7 @@ package com.opus.opus.modules.member.domain; import static jakarta.persistence.FetchType.EAGER; -import static jakarta.persistence.FetchType.LAZY; + import com.opus.opus.global.base.BaseEntity; import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; @@ -58,7 +58,7 @@ public class Member extends BaseEntity { @Builder private Member(final String name, final String email, final String password, final String studentId, - final Set roles) { + final Set roles) { this.name = name; this.email = email; this.password = password; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 868c7998..f8e8f50e 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -3,6 +3,7 @@ USE opus; DROP TABLE IF EXISTS `contest`; DROP TABLE IF EXISTS `contest_award`; DROP TABLE IF EXISTS `contest_category`; +DROP TABLE IF EXISTS `contest_template`; DROP TABLE IF EXISTS `contest_track`; DROP TABLE IF EXISTS `contest_sort`; DROP TABLE IF EXISTS `file`; @@ -51,6 +52,28 @@ CREATE TABLE `contest_category` ( PRIMARY KEY (`id`) ); +CREATE TABLE `contest_template` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) DEFAULT NULL, + `updated_at` datetime(6) DEFAULT NULL, + `contest_id` bigint NOT NULL, + `track_required` bit(1) NOT NULL, + `project_name_required` bit(1) NOT NULL, + `team_name_required` bit(1) NOT NULL, + `leader_required` bit(1) NOT NULL, + `team_members_required` bit(1) NOT NULL, + `professor_required` bit(1) NOT NULL, + `github_path_required` bit(1) NOT NULL, + `you_tube_path_required` bit(1) NOT NULL, + `production_path_required` bit(1) NOT NULL, + `overview_required` bit(1) NOT NULL, + `poster_required` bit(1) NOT NULL, + `images_required` bit(1) NOT NULL, + `is_deleted` bit(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_contest_team_template_contest_id` (`contest_id`) +); + CREATE TABLE `contest_track` ( `id` bigint NOT NULL AUTO_INCREMENT, `created_at` datetime(6) DEFAULT NULL, diff --git a/src/test/java/com/opus/opus/contest/ContestTemplateFixture.java b/src/test/java/com/opus/opus/contest/ContestTemplateFixture.java new file mode 100644 index 00000000..9b9bbe08 --- /dev/null +++ b/src/test/java/com/opus/opus/contest/ContestTemplateFixture.java @@ -0,0 +1,25 @@ +package com.opus.opus.contest; + +import com.opus.opus.modules.contest.domain.Contest; +import com.opus.opus.modules.contest.domain.ContestTemplate; + +public class ContestTemplateFixture { + + public static ContestTemplate createContestTemplate(final Contest contest) { + return ContestTemplate.builder() + .contest(contest) + .trackRequired(true) + .projectNameRequired(true) + .teamNameRequired(true) + .leaderRequired(true) + .teamMembersRequired(true) + .professorRequired(true) + .githubPathRequired(true) + .youTubePathRequired(true) + .productionPathRequired(true) + .overviewRequired(true) + .posterRequired(true) + .imagesRequired(true) + .build(); + } +} diff --git a/src/test/java/com/opus/opus/contest/application/ContestCommandServiceTest.java b/src/test/java/com/opus/opus/contest/application/ContestCommandServiceTest.java index 7e51c333..4c26113f 100644 --- a/src/test/java/com/opus/opus/contest/application/ContestCommandServiceTest.java +++ b/src/test/java/com/opus/opus/contest/application/ContestCommandServiceTest.java @@ -12,20 +12,26 @@ import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_FOUND_CONTEST; import static com.opus.opus.modules.contest.exception.ContestExceptionType.ONLY_CUSTOM_MODE_CAN_CHANGE; import static com.opus.opus.modules.contest.exception.ContestExceptionType.VOTE_END_PRECEDE_VOTE_START; +import static com.opus.opus.modules.contest.exception.ContestTemplateExceptionType.NOT_FOUND_TEMPLATE; import static com.opus.opus.team.TeamFixture.createTeamWithContestIdAndItemOrder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.opus.opus.contest.ContestTemplateFixture; import com.opus.opus.helper.IntegrationTest; import com.opus.opus.modules.contest.application.ContestCommandService; import com.opus.opus.modules.contest.application.dto.request.ContestSortCustomRequest; import com.opus.opus.modules.contest.application.dto.request.ContestSortRequest; +import com.opus.opus.modules.contest.application.dto.request.ContestTemplateRequest; import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; import com.opus.opus.modules.contest.domain.Contest; import com.opus.opus.modules.contest.domain.ContestSort; +import com.opus.opus.modules.contest.domain.ContestTemplate; import com.opus.opus.modules.contest.domain.dao.ContestRepository; import com.opus.opus.modules.contest.domain.dao.ContestSortRepository; +import com.opus.opus.modules.contest.domain.dao.ContestTemplateRepository; import com.opus.opus.modules.contest.exception.ContestException; +import com.opus.opus.modules.contest.exception.ContestTemplateException; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.dao.TeamRepository; import java.time.LocalDateTime; @@ -37,18 +43,18 @@ public class ContestCommandServiceTest extends IntegrationTest { + private static final Integer MAX_VOTES_LIMIT = 5; @Autowired private ContestCommandService contestCommandService; - @Autowired private ContestRepository contestRepository; @Autowired private ContestSortRepository contestSortRepository; @Autowired private TeamRepository teamRepository; - + @Autowired + private ContestTemplateRepository contestTemplateRepository; private Contest contest; - private static final Integer MAX_VOTES_LIMIT = 5; @BeforeEach void setUp() { @@ -221,7 +227,7 @@ void setUp() { contestCommandService.updateContestSort(contest.getId(), new ContestSortRequest(CUSTOM)); final Team teamOne = teamRepository.save(createTeamWithContestIdAndItemOrder(contest.getId(), 1)); final Team teamTwo = teamRepository.save(createTeamWithContestIdAndItemOrder(contest.getId(), 2)); - teamRepository.save(createTeamWithContestIdAndItemOrder(contest.getId(),3)); + teamRepository.save(createTeamWithContestIdAndItemOrder(contest.getId(), 3)); final List requests = List.of(new ContestSortCustomRequest(teamOne.getId(), 2), new ContestSortCustomRequest(teamTwo.getId(), 1)); @@ -244,4 +250,96 @@ void setUp() { contestCommandService.updateContestSortCustom(contest.getId(), requests); }).isInstanceOf(ContestException.class).hasMessage(INVALID_ITEM_ORDER.errorMessage()); } + + @Test + @DisplayName("[성공] 대회 템플릿을 수정한다.") + void 대회_템플릿을_수정한다() { + // given + contestTemplateRepository.save(ContestTemplateFixture.createContestTemplate(contest)); + final ContestTemplateRequest request = new ContestTemplateRequest( + false, false, false, false, false, false, + false, false, false, false, false, false + ); + + // when + contestCommandService.updateContestTemplate(contest.getId(), request); + + // then + final ContestTemplate updatedTemplate = contestTemplateRepository.findByContestId(contest.getId()) + .orElseThrow(); + assertThat(updatedTemplate.getTrackRequired()).isFalse(); + assertThat(updatedTemplate.getProjectNameRequired()).isFalse(); + assertThat(updatedTemplate.getTeamNameRequired()).isFalse(); + assertThat(updatedTemplate.getLeaderRequired()).isFalse(); + assertThat(updatedTemplate.getTeamMembersRequired()).isFalse(); + assertThat(updatedTemplate.getProfessorRequired()).isFalse(); + assertThat(updatedTemplate.getGithubPathRequired()).isFalse(); + assertThat(updatedTemplate.getYouTubePathRequired()).isFalse(); + assertThat(updatedTemplate.getProductionPathRequired()).isFalse(); + assertThat(updatedTemplate.getOverviewRequired()).isFalse(); + assertThat(updatedTemplate.getPosterRequired()).isFalse(); + assertThat(updatedTemplate.getImagesRequired()).isFalse(); + } + + @Test + @DisplayName("[실패] 대회 템플릿 정보가 존재하지 않으면 수정에 실패한다.") + void 대회_템플릿_정보가_존재하지_않으면_수정에_실패한다() { + final ContestTemplateRequest request = new ContestTemplateRequest( + false, false, false, false, false, false, + false, false, false, false, false, false + ); + + assertThatThrownBy(() -> { + contestCommandService.updateContestTemplate(contest.getId(), request); + }).isInstanceOf(ContestTemplateException.class).hasMessage(NOT_FOUND_TEMPLATE.errorMessage()); + } + + @Test + @DisplayName("[성공] 창의융합 카테고리로 템플릿을 생성한다.") + void 창의융합_카테고리로_템플릿을_생성한다() { + // given + final String categoryName = "창의융합공학"; + + // when + contestCommandService.createTemplate(contest, categoryName); + + // then + final ContestTemplate template = contestTemplateRepository.findByContestId(contest.getId()).orElseThrow(); + assertThat(template.getTrackRequired()).isTrue(); + assertThat(template.getProfessorRequired()).isFalse(); + assertThat(template.getYouTubePathRequired()).isFalse(); + } + + @Test + @DisplayName("[성공] 캡스톤 카테고리로 템플릿을 생성한다.") + void 캡스톤_카테고리로_템플릿을_생성한다() { + // given + final String categoryName = "캡스톤디자인"; + + // when + contestCommandService.createTemplate(contest, categoryName); + + // then + final ContestTemplate template = contestTemplateRepository.findByContestId(contest.getId()).orElseThrow(); + assertThat(template.getTrackRequired()).isTrue(); + assertThat(template.getProfessorRequired()).isTrue(); + assertThat(template.getYouTubePathRequired()).isTrue(); + assertThat(template.getPosterRequired()).isFalse(); + } + + @Test + @DisplayName("[성공] 기본 카테고리로 템플릿을 생성한다.") + void 기본_카테고리로_템플릿을_생성한다() { + // given + final String categoryName = "기타"; + + // when + contestCommandService.createTemplate(contest, categoryName); + + // then + final ContestTemplate template = contestTemplateRepository.findByContestId(contest.getId()).orElseThrow(); + assertThat(template.getTrackRequired()).isFalse(); + assertThat(template.getProjectNameRequired()).isFalse(); + assertThat(template.getImagesRequired()).isFalse(); + } } 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 0fd28e18..9d30dfbe 100644 --- a/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java +++ b/src/test/java/com/opus/opus/contest/application/ContestQueryServiceTest.java @@ -2,6 +2,7 @@ import static com.opus.opus.member.MemberFixture.createMemberWithUniqueNum; import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_FOUND_CONTEST; +import static com.opus.opus.modules.contest.exception.ContestTemplateExceptionType.NOT_FOUND_TEMPLATE; import static com.opus.opus.team.TeamFixture.createTeamWithContestId; import static com.opus.opus.team.TeamVoteFixture.createTeamVote; import static java.time.LocalDateTime.now; @@ -9,27 +10,30 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.opus.opus.contest.ContestFixture; +import com.opus.opus.contest.ContestTemplateFixture; import com.opus.opus.helper.IntegrationTest; import com.opus.opus.member.MemberFixture; import com.opus.opus.modules.contest.application.ContestQueryService; -import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSubmissionResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestTemplateResponse; +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.VotePeriodResponse; import com.opus.opus.modules.contest.domain.Contest; import com.opus.opus.modules.contest.domain.dao.ContestRepository; +import com.opus.opus.modules.contest.domain.dao.ContestTemplateRepository; import com.opus.opus.modules.contest.exception.ContestException; +import com.opus.opus.modules.contest.exception.ContestTemplateException; import com.opus.opus.modules.member.domain.Member; import com.opus.opus.modules.member.domain.dao.MemberRepository; +import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; import com.opus.opus.modules.team.domain.Team; import com.opus.opus.modules.team.domain.dao.TeamRepository; import com.opus.opus.modules.team.domain.dao.TeamVoteRepository; -import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse; import com.opus.opus.team.TeamFixture; import com.opus.opus.team.TeamVoteFixture; - import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -51,6 +55,8 @@ public class ContestQueryServiceTest extends IntegrationTest { private TeamRepository teamRepository; @Autowired private TeamVoteRepository teamVoteRepository; + @Autowired + private ContestTemplateRepository contestTemplateRepository; private Contest contest; private Team team; @@ -130,7 +136,7 @@ void setUp() { assertThat(contestVoteLogResponses.size()).isEqualTo(3); assertThat(contestVoteLogResponses.get(0).votedAt()).isAfterOrEqualTo(contestVoteLogResponses.get(1).votedAt()); } - + @Test @DisplayName("[성공] 사용자의 남은 투표 개수를 조회할 수 있다.") void 사용자의_남은_투표_개수를_조회할_수_있다() { @@ -266,7 +272,8 @@ void setUp() { void 팀이_없는_대회는_빈_리스트를_반환한다() { final Contest emptyContest = contestRepository.save(ContestFixture.createContest()); - final List responseList = contestQueryService.getTeamSubmissions(emptyContest.getId()); + final List responseList = contestQueryService.getTeamSubmissions( + emptyContest.getId()); assertThat(responseList).isEmpty(); } @@ -280,4 +287,33 @@ void setUp() { .isInstanceOf(ContestException.class) .hasMessage(NOT_FOUND_CONTEST.errorMessage()); } + + @Test + @DisplayName("[성공] 대회 템플릿을 조회한다.") + void 대회_템플릿을_조회한다() { + contestTemplateRepository.save(ContestTemplateFixture.createContestTemplate(contest)); + + final ContestTemplateResponse response = contestQueryService.getContestTemplate(contest.getId()); + + assertThat(response.trackRequired()).isTrue(); + assertThat(response.projectNameRequired()).isTrue(); + assertThat(response.teamNameRequired()).isTrue(); + assertThat(response.leaderRequired()).isTrue(); + assertThat(response.teamMembersRequired()).isTrue(); + assertThat(response.professorRequired()).isTrue(); + assertThat(response.githubPathRequired()).isTrue(); + assertThat(response.youTubePathRequired()).isTrue(); + assertThat(response.productionPathRequired()).isTrue(); + assertThat(response.overviewRequired()).isTrue(); + assertThat(response.posterRequired()).isTrue(); + assertThat(response.imagesRequired()).isTrue(); + } + + @Test + @DisplayName("[실패] 대회 템플릿이 존재하지 않으면 예외가 발생한다.") + void 대회_템플릿이_존재하지_않으면_예외가_발생한다() { + assertThatThrownBy(() -> { + contestQueryService.getContestTemplate(contest.getId()); + }).isInstanceOf(ContestTemplateException.class).hasMessage(NOT_FOUND_TEMPLATE.errorMessage()); + } } 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 492aecbf..5f5f1fa6 100644 --- a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java @@ -38,14 +38,16 @@ 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; +import com.opus.opus.modules.contest.application.dto.request.ContestTemplateRequest; import com.opus.opus.modules.contest.application.dto.request.ContestVotesLimitRequest; import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; import com.opus.opus.modules.contest.application.dto.response.ContestRankingResponse; import com.opus.opus.modules.contest.application.dto.response.ContestResponse; -import com.opus.opus.modules.contest.application.dto.response.ContestVoteStatisticsResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSortResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVoteLogResponse; import com.opus.opus.modules.contest.application.dto.response.ContestSubmissionResponse; +import com.opus.opus.modules.contest.application.dto.response.ContestTemplateResponse; +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.modules.contest.exception.ContestException; @@ -411,7 +413,8 @@ void setUp() { .andExpect(status().isOk()) .andDo(document("create-contest", requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( stringFieldWithPath("contestName", "대회 이름"), @@ -464,7 +467,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( stringFieldWithPath("contestName", "대회 이름"), @@ -486,7 +490,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ) )); } @@ -533,7 +538,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( stringFieldWithPath("mode", "수정할 대회 정렬 모드") @@ -555,7 +561,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), responseFields( stringFieldWithPath("currentMode", "현재 적용되어 있는 모드 정보") @@ -712,7 +719,8 @@ void setUp() { parameterWithName("size").description("페이지 크기").optional() ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), responseFields( arrayFieldWithPath("content[]", "투표 로그 목록 (최신순)"), @@ -885,4 +893,121 @@ void setUp() { ) )); } + + @Test + @DisplayName("[성공] 대회의 템플릿을 조회한다.") + void 대회의_템플릿_설정을_조회한다() throws Exception { + final ContestTemplateResponse response = new ContestTemplateResponse( + true, true, true, true, true, true, + true, true, true, true, true, true + ); + + when(contestQueryService.getContestTemplate(any())).thenReturn(response); + + mockMvc.perform(get("/contests/{contestId}/template", 1)) + .andExpect(status().isOk()) + .andDo(document("get-contest-template", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + responseFields( + booleanFieldWithPath("trackRequired", "트랙/분과 필수 여부"), + booleanFieldWithPath("projectNameRequired", "프로젝트명 필수 여부"), + booleanFieldWithPath("teamNameRequired", "팀명 필수 여부"), + booleanFieldWithPath("leaderRequired", "팀장 필수 여부"), + booleanFieldWithPath("teamMembersRequired", "팀원 필수 여부"), + booleanFieldWithPath("professorRequired", "지도교수 필수 여부"), + booleanFieldWithPath("githubPathRequired", "GitHub 링크 필수 여부"), + booleanFieldWithPath("youTubePathRequired", "YouTube 링크 필수 여부"), + booleanFieldWithPath("productionPathRequired", "배포 링크 필수 여부"), + booleanFieldWithPath("overviewRequired", "프로젝트 개요 필수 여부"), + booleanFieldWithPath("posterRequired", "포스터 필수 여부"), + booleanFieldWithPath("imagesRequired", "이미지 필수 여부") + ) + )); + } + + @Test + @DisplayName("[실패] 존재하지 않는 대회의 템플릿 조회 시 404 에러를 반환한다.") + void 존재하지_않는_대회의_템플릿_조회_시_에러를_반환한다() throws Exception { + willThrow(new ContestException(NOT_FOUND_CONTEST)) + .given(contestQueryService) + .getContestTemplate(any()); + + mockMvc.perform(get("/contests/{contestId}/template", 999)) + .andExpect(status().isNotFound()) + .andDo(document("get-contest-template-fail-not-found", + pathParameters( + parameterWithName("contestId").description("존재하지 않는 대회 ID") + ) + )); + } + + @Test + @DisplayName("[성공] 대회의 템플릿을 수정한다.") + void 대회의_템플릿_설정을_수정한다() throws Exception { + final ContestTemplateRequest request = new ContestTemplateRequest( + false, false, false, false, false, false, + false, false, false, false, false, false + ); + + doNothing().when(contestCommandService).updateContestTemplate(any(), any()); + + mockMvc.perform(put("/contests/{contestId}/template", 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNoContent()) + .andDo(document("update-contest-template", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) + ), + requestFields( + booleanFieldWithPath("trackRequired", "트랙/분과 필수 여부"), + booleanFieldWithPath("projectNameRequired", "프로젝트명 필수 여부"), + booleanFieldWithPath("teamNameRequired", "팀명 필수 여부"), + booleanFieldWithPath("leaderRequired", "팀장 필수 여부"), + booleanFieldWithPath("teamMembersRequired", "팀원 필수 여부"), + booleanFieldWithPath("professorRequired", "지도교수 필수 여부"), + booleanFieldWithPath("githubPathRequired", "GitHub 링크 필수 여부"), + booleanFieldWithPath("youTubePathRequired", "YouTube 링크 필수 여부"), + booleanFieldWithPath("productionPathRequired", "배포 링크 필수 여부"), + booleanFieldWithPath("overviewRequired", "프로젝트 개요 필수 여부"), + booleanFieldWithPath("posterRequired", "포스터 필수 여부"), + booleanFieldWithPath("imagesRequired", "이미지 필수 여부") + ) + )); + } + + @Test + @DisplayName("[실패] 존재하지 않는 대회의 템플릿 수정 시 404 에러를 반환한다.") + void 존재하지_않는_대회의_템플릿_수정_시_에러를_반환한다() throws Exception { + final ContestTemplateRequest request = new ContestTemplateRequest( + false, false, false, false, false, false, + false, false, false, false, false, false + ); + + willThrow(new ContestException(NOT_FOUND_CONTEST)) + .given(contestCommandService) + .updateContestTemplate(any(), any()); + + mockMvc.perform(put("/contests/{contestId}/template", 999) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andDo(document("update-contest-template-fail-not-found", + pathParameters( + parameterWithName("contestId").description("존재하지 않는 대회 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) + ) + )); + } } diff --git a/src/test/java/com/opus/opus/restdocs/docs/TeamApiDocsTest.java b/src/test/java/com/opus/opus/restdocs/docs/TeamApiDocsTest.java index 83d616c7..356f88ff 100644 --- a/src/test/java/com/opus/opus/restdocs/docs/TeamApiDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/docs/TeamApiDocsTest.java @@ -72,7 +72,8 @@ void setUp() { final Long teamId = 999L; when(teamQueryService.getPosterImage(any())) - .thenThrow(new FileException(FileExceptionType.NOT_EXISTS_MATCHING_IMAGE_ID)); // Assuming logic or specific TeamException. checking service... + .thenThrow(new FileException( + FileExceptionType.NOT_EXISTS_MATCHING_IMAGE_ID)); // Assuming logic or specific TeamException. checking service... // Wait, service uses teamConvenience.validateExistTeam(teamId). // I need to mock the service call to throw the exception that represents "Team Not Found" or "Image Not Found". // In TeamQueryService.getImage: @@ -84,9 +85,9 @@ void setUp() { // But the prompt asked for "failure cases". // I'll stick to FileException for consistency with previous file tests. } - + // RE-WRITING THE REPLACEMENT TO BE CORRECT - + @Test @DisplayName("[실패] 등록되지 않은 팀의 포스터 이미지를 조회하면 실패한다.") void 팀의_포스터_이미지_조회_실패_이미지없음() throws Exception { @@ -150,7 +151,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ), requestParts( partWithName("image").description("등록할 포스터 이미지 (모든 이미지 형식 지원)") @@ -174,7 +176,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ) )); } @@ -262,7 +265,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ), requestParts( partWithName("image").description("등록할 썸네일 이미지 (모든 이미지 형식 지원)") @@ -286,7 +290,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ) )); } @@ -345,7 +350,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ), requestParts( partWithName("images").description("등록할 프리뷰 이미지 목록 (리스트)") @@ -373,7 +379,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ), requestFields( arrayFieldWithPath("imageIds", "삭제할 프리뷰 이미지 ID 리스트") @@ -407,7 +414,8 @@ void setUp() { parameterWithName("teamId").description("팀 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "(teamLeader/admin/teamMember)")) ), requestParts( partWithName("images").description("등록할 프리뷰 이미지")