Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions src/main/java/com/opus/opus/docs/asciidoc/opus.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ link:./member.html[회원 API]
link:./team.html[팀 API]

link:./team-comment.html[팀 댓글 API]
link:./team-vote.html[팀 투표 API]

link:./team-member.html[팀원 API]

Expand Down
203 changes: 203 additions & 0 deletions src/main/java/com/opus/opus/docs/asciidoc/team-vote.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]

= TEAM VOTE API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3
:sectnums:

== API 목록

link:./opus.html[API 목록으로 돌아가기]

== `PUT`: 팀 투표 토글

해당 팀에 대해 투표 상태를 토글합니다.

* `isVoted: true` → 투표 등록
* `isVoted: false` → 투표 취소

NOTE: 대회 기간 중에는 투표만 보이고, 대회 기간이 아니라면 좋아요가 보입니다.

NOTE: 해당 대회의 최대 투표 개수 이상으로 투표를 등록할 수 없습니다.

.Path Parameters
include::{snippets}/toggle-team-vote/path-parameters.adoc[]

.HTTP Request Headers
include::{snippets}/toggle-team-vote/request-headers.adoc[]

.Request Fields
include::{snippets}/toggle-team-vote/request-fields.adoc[]

.Response Fields
include::{snippets}/toggle-team-vote/response-fields.adoc[]

=== 시나리오 1: TeamVote 데이터가 없는 경우

특정 멤버가 특정 팀에 대해 투표 API를 처음 호출하면, TeamVote 테이블에 데이터가 새로 생성됩니다.

[cols="1,2,1"]
|===
|Request isVoted |응답 메시지 |HTTP 상태 코드

|true
|투표가 등록되었습니다.
|200 OK

|false
|아직 투표하지 않은 팀입니다.
|400 Bad Request
|===

isVoted: true → 투표 등록

include::{snippets}/toggle-team-vote/http-request.adoc[]

include::{snippets}/toggle-team-vote/http-response.adoc[]

=== 시나리오 2: TeamVote 데이터가 있는 경우

이미 해당 팀에 대한 투표 기록이 있는 경우, 상태에 따라 토글됩니다.

[cols="1,1,2,1"]
|===
|현재 isVoted |Request isVoted |응답 메시지 |HTTP 상태 코드

|true
|true
|이미 투표한 팀입니다.
|400 Bad Request

|true
|false
|투표가 취소되었습니다.
|200 OK

|false
|true
|투표가 등록되었습니다.
|200 OK

|false
|false
|이미 투표를 취소한 팀입니다.
|400 Bad Request
|===

투표 취소 (isVoted: true → false)

include::{snippets}/cancel-team-vote/http-request.adoc[]

include::{snippets}/cancel-team-vote/http-response.adoc[]

=== ⚠️ 실패 케이스

.❌ Case 1: 존재하지 않는 팀

[%collapsible]

====

include::{snippets}/toggle-team-vote-fail-not-found/http-request.adoc[]

include::{snippets}/toggle-team-vote-fail-not-found/http-response.adoc[]

====

.❌ Case 2: 이미 투표한 팀

[%collapsible]

====

include::{snippets}/toggle-team-vote-fail-already-voted/http-request.adoc[]

include::{snippets}/toggle-team-vote-fail-already-voted/http-response.adoc[]

====

.❌ Case 3: 이미 투표 취소한 팀

[%collapsible]

====

include::{snippets}/toggle-team-vote-fail-already-unvoted/http-request.adoc[]

include::{snippets}/toggle-team-vote-fail-already-unvoted/http-response.adoc[]

====

.❌ Case 4: 투표한 적 없는 팀에 취소 요청

[%collapsible]

====

include::{snippets}/toggle-team-vote-fail-not-voted-yet/http-request.adoc[]

include::{snippets}/toggle-team-vote-fail-not-voted-yet/http-response.adoc[]

====

.❌ Case 5: 최대 투표 수 초과

[%collapsible]

====

include::{snippets}/toggle-team-vote-fail-limit-exceeded/http-request.adoc[]

include::{snippets}/toggle-team-vote-fail-limit-exceeded/http-response.adoc[]

====

.❌ Case 6: 투표 기간이 아님

[%collapsible]

====

include::{snippets}/toggle-team-vote-fail-not-vote-period/http-request.adoc[]

include::{snippets}/toggle-team-vote-fail-not-vote-period/http-response.adoc[]

====

== `GET`: 사용자 투표 개수 조회

현재 사용자가 특정 대회에서 남은 투표 가능 횟수와 최대 투표 허용 개수를 조회합니다.

.Query Parameters
include::{snippets}/get-member-vote-count/query-parameters.adoc[]

.HTTP Request Headers
include::{snippets}/get-member-vote-count/request-headers.adoc[]

.HTTP Request
include::{snippets}/get-member-vote-count/http-request.adoc[]

.HTTP Response
include::{snippets}/get-member-vote-count/http-response.adoc[]

.Response Fields
include::{snippets}/get-member-vote-count/response-fields.adoc[]

=== ⚠️ 실패 케이스

.❌ Case 1: 존재하지 않는 대회

[%collapsible]

====

include::{snippets}/get-member-vote-count-fail-not-found/http-request.adoc[]

include::{snippets}/get-member-vote-count-fail-not-found/http-response.adoc[]

====
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,7 +12,10 @@
import com.opus.opus.modules.contest.application.dto.response.ContestResponse;
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.TeamVoteQueryService;
import com.opus.opus.modules.team.application.dto.ImageResponse;
import com.opus.opus.modules.team.application.dto.response.MemberVoteCountResponse;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -40,6 +44,7 @@ public class ContestController {

private final ContestCommandService contestCommandService;
private final ContestQueryService contestQueryService;
private final TeamVoteQueryService teamVoteCommandService;

@GetMapping("/{contestId}/image/banner")
public ResponseEntity<Resource> getContestBanner(@PathVariable final Long contestId) {
Expand Down Expand Up @@ -134,4 +139,12 @@ public ResponseEntity<ContestVotesLimitResponse> getMaxVotesLimit(@PathVariable
final ContestVotesLimitResponse response = contestQueryService.getMaxVotesLimit(contestId);
return ResponseEntity.ok(response);
}

@Secured({"ROLE_회원", "ROLE_관리자"})
@GetMapping("/{contestId}/votes/me")
public ResponseEntity<MemberVoteCountResponse> getMemberVoteCount(@PathVariable Long contestId,
@LoginMember Member member) {
MemberVoteCountResponse response = teamVoteCommandService.getMemberVoteCount(member.getId(), contestId);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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_FOUND_CONTEST;
import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_VOTE_PERIOD_NOW;

import com.opus.opus.modules.contest.domain.Contest;
import com.opus.opus.modules.contest.domain.dao.ContestRepository;
Expand Down Expand Up @@ -47,4 +48,10 @@ public long countCurrentContests() {
public List<Contest> getCurrentContests() {
return contestRepository.findAllByIsCurrentTrue();
}

public void validateVotingPeriod(final Contest contest) {
if (!contest.isVotingPeriod()) {
throw new ContestException(NOT_VOTE_PERIOD_NOW);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public enum ContestExceptionType implements BaseExceptionType {
CATEGORY_HAS_CONTEST(HttpStatus.CONFLICT, "해당 카테고리에 속한 대회가 존재합니다."),
CONTEST_NAME_ALREADY_EXIST(HttpStatus.CONFLICT, "동일한 대회명이 있습니다."),
VOTE_END_PRECEDE_VOTE_START(HttpStatus.BAD_REQUEST, "투표 종료가 투표 시작보다 빠를 수 없습니다."),
CANNOT_CHANGE_VOTES_DURING_VOTING_PERIOD(HttpStatus.BAD_REQUEST, "투표 진행중에는 최대 투표 개수를 변경할 수 없습니다.")
CANNOT_CHANGE_VOTES_DURING_VOTING_PERIOD(HttpStatus.BAD_REQUEST, "투표 진행중에는 최대 투표 개수를 변경할 수 없습니다."),
NOT_VOTE_PERIOD_NOW(HttpStatus.BAD_REQUEST, "지금은 투표 기간이 아닙니다."),
;

private final HttpStatus httpStatus;
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/opus/opus/modules/team/api/TeamController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.opus.opus.modules.team.api;

import com.opus.opus.global.security.annotation.LoginMember;
import com.opus.opus.modules.member.domain.Member;
import com.opus.opus.modules.team.application.TeamCommandService;
import com.opus.opus.modules.team.application.TeamQueryService;
import com.opus.opus.modules.team.application.TeamVoteCommandService;
import com.opus.opus.modules.team.application.dto.ImageResponse;
import com.opus.opus.modules.team.application.dto.request.PreviewDeleteRequest;
import com.opus.opus.modules.team.application.dto.request.TeamVoteToggleRequest;
import com.opus.opus.modules.team.application.dto.response.TeamVoteToggleResponse;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -16,6 +21,7 @@
import org.springframework.web.bind.annotation.GetMapping;
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;
Expand All @@ -30,6 +36,7 @@ public class TeamController {

private final TeamQueryService teamQueryService;
private final TeamCommandService teamCommandService;
private final TeamVoteCommandService teamVoteCommandService;

@GetMapping("/{teamId}/image/{imageId}")
public ResponseEntity<Resource> getPreviewImage(@PathVariable final Long teamId, @PathVariable final Long imageId) {
Expand Down Expand Up @@ -80,6 +87,15 @@ public ResponseEntity<Void> deleteThumbnailImage(@PathVariable final Long teamId
return ResponseEntity.noContent().build();
}

@Secured({"ROLE_회원", "ROLE_관리자"})
@PutMapping("/{teamId}/votes")
public ResponseEntity<TeamVoteToggleResponse> toggleVote(@PathVariable Long teamId,
@RequestBody @Valid TeamVoteToggleRequest request,
@LoginMember Member member) {
TeamVoteToggleResponse response = teamVoteCommandService.toggleVote(member.getId(), teamId, request.isVoted());
return ResponseEntity.ok(response);
}

@GetMapping("/{teamId}/image/posters")
public ResponseEntity<Resource> getPosterImage(@PathVariable final Long teamId) {
final ImageResponse imageResponse = teamQueryService.getPosterImage(teamId);
Expand Down
Loading