From 43cba4bf612b6396e03d15b2e3e50252507b43c2 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 09:14:34 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20controller=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/poll/VoteController.java | 34 +++++++++++++++++++ .../dto/poll/request/VoteRequest.java | 13 +++++++ .../dto/poll/response/VoteCreateResponse.java | 13 +++++++ .../poll/response/VoterPollInfoResponse.java | 16 +++++++++ 4 files changed, 76 insertions(+) create mode 100644 src/main/java/com/debatetimer/controller/poll/VoteController.java create mode 100644 src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java create mode 100644 src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java create mode 100644 src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java diff --git a/src/main/java/com/debatetimer/controller/poll/VoteController.java b/src/main/java/com/debatetimer/controller/poll/VoteController.java new file mode 100644 index 00000000..8351e6ca --- /dev/null +++ b/src/main/java/com/debatetimer/controller/poll/VoteController.java @@ -0,0 +1,34 @@ +package com.debatetimer.controller.poll; + +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +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.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class VoteController { + + @GetMapping("/api/polls/{pollId}/votes") + @ResponseStatus(HttpStatus.OK) + public VoterPollInfoResponse getVotersPollInfo(@PathVariable int pollId) { + + return null; + } + + @PostMapping("/api/polls/{pollId}/votes") + @ResponseStatus(HttpStatus.CREATED) + public VoteCreateResponse votePoll( + @PathVariable int pollId, + @RequestBody @Valid VoteRequest voteRequest + ) { + + return null; + } +} diff --git a/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java b/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java new file mode 100644 index 00000000..6fe447f8 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java @@ -0,0 +1,13 @@ +package com.debatetimer.dto.poll.request; + +import com.debatetimer.domain.poll.VoteTeam; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record VoteRequest( + @NotBlank String name, + @NotBlank String participantCode, + @NotNull VoteTeam team +) { + +} diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java new file mode 100644 index 00000000..c1d37e86 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java @@ -0,0 +1,13 @@ +package com.debatetimer.dto.poll.response; + +import com.debatetimer.domain.poll.VoteTeam; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record VoteCreateResponse( + long id, + @NotBlank String name, + @NotBlank String participantCode, + @NotNull VoteTeam team +) { +} diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java new file mode 100644 index 00000000..ef7407e4 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java @@ -0,0 +1,16 @@ +package com.debatetimer.dto.poll.response; + +import com.debatetimer.domain.poll.PollStatus; + +public record VoterPollInfoResponse( + long id, + PollStatus status, + String prosTeamName, + String consTeamName, + String participantCode, + long totalCount, + long prosCount, + long consCount +) { + +} From d33dd4cf1a31ce105a60e5b08928286c56773d29 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 09:37:05 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20service=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/poll/Vote.java | 4 ++ .../poll/PollDomainRepository.java | 17 ++++----- .../poll/VoteDomainRepository.java | 11 ++++++ .../dto/poll/request/VoteRequest.java | 2 +- .../dto/poll/response/VoteCreateResponse.java | 5 +++ .../poll/response/VoterPollInfoResponse.java | 17 ++++++++- .../debatetimer/entity/poll/VoteEntity.java | 6 +++ .../repository/poll/PollRepository.java | 12 ++++++ .../debatetimer/service/poll/VoteService.java | 38 +++++++++++++++++++ 9 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/debatetimer/service/poll/VoteService.java diff --git a/src/main/java/com/debatetimer/domain/poll/Vote.java b/src/main/java/com/debatetimer/domain/poll/Vote.java index a69f946a..6d14cf5f 100644 --- a/src/main/java/com/debatetimer/domain/poll/Vote.java +++ b/src/main/java/com/debatetimer/domain/poll/Vote.java @@ -13,6 +13,10 @@ public class Vote { private final ParticipantName name; private final ParticipateCode code; + public Vote(long pollId, VoteTeam team, String name, String code) { + this(null, pollId, team, name, code); + } + public Vote(Long id, long pollId, VoteTeam team, String name, String code) { this(id, pollId, team, new ParticipantName(name), new ParticipateCode(code)); } diff --git a/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java index fa46adfb..e35ceca4 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java @@ -2,8 +2,6 @@ import com.debatetimer.domain.poll.Poll; import com.debatetimer.entity.poll.PollEntity; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.repository.poll.PollRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -24,19 +22,20 @@ public Poll create(Poll poll) { @Transactional(readOnly = true) public Poll getByIdAndMemberId(long id, long memberId) { - return findPoll(id, memberId) + return pollRepository.getByIdAndMemberId(id, memberId) + .toDomain(); + } + + @Transactional(readOnly = true) + public Poll getById(long id) { + return pollRepository.getById(id) .toDomain(); } @Transactional public Poll finishPoll(long pollId, long memberId) { - PollEntity pollEntity = findPoll(pollId, memberId); + PollEntity pollEntity = pollRepository.getByIdAndMemberId(pollId, memberId); pollEntity.updateToDone(); return pollEntity.toDomain(); } - - private PollEntity findPoll(long pollId, long memberId) { - return pollRepository.findByIdAndMemberId(pollId, memberId) - .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); - } } diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index 97f8dd1f..97ed7829 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -1,8 +1,11 @@ package com.debatetimer.domainrepository.poll; +import com.debatetimer.domain.poll.Vote; import com.debatetimer.domain.poll.VoteInfo; import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.entity.poll.PollEntity; import com.debatetimer.entity.poll.VoteEntity; +import com.debatetimer.repository.poll.PollRepository; import com.debatetimer.repository.poll.VoteRepository; import java.util.List; import java.util.Map; @@ -14,6 +17,7 @@ @RequiredArgsConstructor public class VoteDomainRepository { + private final PollRepository pollRepository; private final VoteRepository voteRepository; public VoteInfo findVoteInfoByPollId(long pollId) { @@ -28,4 +32,11 @@ private VoteInfo countVotes(long pollId, List voteEntities) { long consCount = teamCount.getOrDefault(VoteTeam.CONS, 0L); return new VoteInfo(pollId, prosCount, consCount); } + + public Vote vote(Vote vote) { + PollEntity pollEntity = pollRepository.getById(vote.getPollId()); + VoteEntity voteEntity = new VoteEntity(vote, pollEntity); + return voteRepository.save(voteEntity) + .toDomain(); + } } diff --git a/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java b/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java index 6fe447f8..ccf22242 100644 --- a/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java +++ b/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java @@ -6,7 +6,7 @@ public record VoteRequest( @NotBlank String name, - @NotBlank String participantCode, + @NotBlank String participateCode, @NotNull VoteTeam team ) { diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java index c1d37e86..cdbe4f23 100644 --- a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java +++ b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java @@ -1,5 +1,6 @@ package com.debatetimer.dto.poll.response; +import com.debatetimer.domain.poll.Vote; import com.debatetimer.domain.poll.VoteTeam; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -10,4 +11,8 @@ public record VoteCreateResponse( @NotBlank String participantCode, @NotNull VoteTeam team ) { + + public VoteCreateResponse(Vote vote) { + this(vote.getId(), vote.getName().getValue(), vote.getCode().getValue(), vote.getTeam()); + } } diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java index ef7407e4..acf86030 100644 --- a/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java +++ b/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java @@ -1,16 +1,31 @@ package com.debatetimer.dto.poll.response; +import com.debatetimer.domain.poll.ParticipateCode; +import com.debatetimer.domain.poll.Poll; import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteInfo; public record VoterPollInfoResponse( long id, PollStatus status, String prosTeamName, String consTeamName, - String participantCode, + String participateCode, long totalCount, long prosCount, long consCount ) { + public VoterPollInfoResponse(Poll poll, VoteInfo voteInfo, ParticipateCode code) { + this( + poll.getId(), + poll.getStatus(), + poll.getProsTeamName().getValue(), + poll.getConsTeamName().getValue(), + code.getValue(), + voteInfo.getTotalCount(), + voteInfo.getProsCount(), + voteInfo.getConsCount() + ); + } } diff --git a/src/main/java/com/debatetimer/entity/poll/VoteEntity.java b/src/main/java/com/debatetimer/entity/poll/VoteEntity.java index 47dd0a2c..ed55e5ed 100644 --- a/src/main/java/com/debatetimer/entity/poll/VoteEntity.java +++ b/src/main/java/com/debatetimer/entity/poll/VoteEntity.java @@ -2,6 +2,7 @@ import com.debatetimer.domain.poll.Vote; import com.debatetimer.domain.poll.VoteTeam; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -43,8 +44,13 @@ public class VoteEntity { private String name; @NotBlank + @Column(unique = true) private String participantCode; + public VoteEntity(Vote vote, PollEntity pollEntity) { + this(vote.getId(), pollEntity, vote.getTeam(), vote.getName().getValue(), vote.getCode().getValue()); + } + public Vote toDomain() { return new Vote(id, poll.getId(), team, name, participantCode); } diff --git a/src/main/java/com/debatetimer/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java index 121d1627..22c6db56 100644 --- a/src/main/java/com/debatetimer/repository/poll/PollRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -1,10 +1,22 @@ package com.debatetimer.repository.poll; import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface PollRepository extends JpaRepository { Optional findByIdAndMemberId(long id, long memberId); + + default PollEntity getById(long id) { + return findById(id) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); + } + + default PollEntity getByIdAndMemberId(long id, long memberId) { + return findByIdAndMemberId(id, memberId) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); + } } diff --git a/src/main/java/com/debatetimer/service/poll/VoteService.java b/src/main/java/com/debatetimer/service/poll/VoteService.java new file mode 100644 index 00000000..c5283eba --- /dev/null +++ b/src/main/java/com/debatetimer/service/poll/VoteService.java @@ -0,0 +1,38 @@ +package com.debatetimer.service.poll; + +import com.debatetimer.domain.poll.ParticipateCode; +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.Vote; +import com.debatetimer.domain.poll.VoteInfo; +import com.debatetimer.domainrepository.poll.PollDomainRepository; +import com.debatetimer.domainrepository.poll.VoteDomainRepository; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class VoteService { + + private final VoteDomainRepository voteDomainRepository; + private final PollDomainRepository pollDomainRepository; + + @Transactional + public VoteCreateResponse vote(long pollId, VoteRequest voteRequest) { + Vote vote = new Vote(pollId, voteRequest.team(), voteRequest.name(), voteRequest.participateCode()); + Vote savedVote = voteDomainRepository.vote(vote); + return new VoteCreateResponse(savedVote); + } + + @Transactional(readOnly = true) + public VoterPollInfoResponse getVoterPollInfo(long pollId) { + Poll poll = pollDomainRepository.getById(pollId); + VoteInfo voteInfo = voteDomainRepository.findVoteInfoByPollId(pollId); + ParticipateCode code = new ParticipateCode(UUID.randomUUID().toString()); + return new VoterPollInfoResponse(poll, voteInfo, code); + } +} From dd6ac7c725716041bbf3a09be1c7812d1baba515 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 09:46:43 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=EA=B2=80=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domainrepository/poll/VoteDomainRepository.java | 5 +++++ .../java/com/debatetimer/entity/poll/VoteEntity.java | 11 +++++++---- .../exception/errorcode/ClientErrorCode.java | 1 + .../debatetimer/repository/poll/VoteRepository.java | 3 +++ .../com/debatetimer/service/poll/VoteService.java | 10 ++++++++++ .../V12__add_participate_code_unique_constraint.sql | 5 +++++ 6 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index 97ed7829..0e0960ef 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -1,5 +1,6 @@ package com.debatetimer.domainrepository.poll; +import com.debatetimer.domain.poll.ParticipateCode; import com.debatetimer.domain.poll.Vote; import com.debatetimer.domain.poll.VoteInfo; import com.debatetimer.domain.poll.VoteTeam; @@ -33,6 +34,10 @@ private VoteInfo countVotes(long pollId, List voteEntities) { return new VoteInfo(pollId, prosCount, consCount); } + public boolean alreadyVoted(ParticipateCode code) { + return voteRepository.existsByParticipantCode(code); + } + public Vote vote(Vote vote) { PollEntity pollEntity = pollRepository.getById(vote.getPollId()); VoteEntity voteEntity = new VoteEntity(vote, pollEntity); diff --git a/src/main/java/com/debatetimer/entity/poll/VoteEntity.java b/src/main/java/com/debatetimer/entity/poll/VoteEntity.java index ed55e5ed..0b0f26c7 100644 --- a/src/main/java/com/debatetimer/entity/poll/VoteEntity.java +++ b/src/main/java/com/debatetimer/entity/poll/VoteEntity.java @@ -13,6 +13,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; @@ -22,7 +23,9 @@ @Entity @Getter -@Table(name = "vote") +@Table(name = "vote", uniqueConstraints = { + @UniqueConstraint(columnNames = {"poll_id", "participate_code"}) +}) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class VoteEntity { @@ -44,14 +47,14 @@ public class VoteEntity { private String name; @NotBlank - @Column(unique = true) - private String participantCode; + @Column(name = "participate_code") + private String participateCode; public VoteEntity(Vote vote, PollEntity pollEntity) { this(vote.getId(), pollEntity, vote.getTeam(), vote.getName().getValue(), vote.getCode().getValue()); } public Vote toDomain() { - return new Vote(id, poll.getId(), team, name, participantCode); + return new Vote(id, poll.getId(), team, name, participateCode); } } diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index f0672c70..3a57e7de 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -50,6 +50,7 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_POLL_PARTICIPANT_NAME(HttpStatus.BAD_REQUEST, "잘못된 투표자 이름입니다"), INVALID_POLL_PARTICIPANT_CODE(HttpStatus.BAD_REQUEST, "잘못된 투표참여 코드입니다"), + ALREADY_VOTED_PARTICIPANT(HttpStatus.BAD_REQUEST, "이미 참여한 투표자 입니다"), TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "테이블을 소유한 회원이 아닙니다."), diff --git a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java index a9709200..29573722 100644 --- a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java @@ -1,5 +1,6 @@ package com.debatetimer.repository.poll; +import com.debatetimer.domain.poll.ParticipateCode; import com.debatetimer.entity.poll.VoteEntity; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,4 +8,6 @@ public interface VoteRepository extends JpaRepository { List findAllByPollId(long pollId); + + boolean existsByParticipantCode(ParticipateCode code); } diff --git a/src/main/java/com/debatetimer/service/poll/VoteService.java b/src/main/java/com/debatetimer/service/poll/VoteService.java index c5283eba..2eccb4cd 100644 --- a/src/main/java/com/debatetimer/service/poll/VoteService.java +++ b/src/main/java/com/debatetimer/service/poll/VoteService.java @@ -9,6 +9,8 @@ import com.debatetimer.dto.poll.request.VoteRequest; import com.debatetimer.dto.poll.response.VoteCreateResponse; import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,11 +25,19 @@ public class VoteService { @Transactional public VoteCreateResponse vote(long pollId, VoteRequest voteRequest) { + validateAlreadyVoted(voteRequest.participateCode()); Vote vote = new Vote(pollId, voteRequest.team(), voteRequest.name(), voteRequest.participateCode()); Vote savedVote = voteDomainRepository.vote(vote); return new VoteCreateResponse(savedVote); } + private void validateAlreadyVoted(String participateCode) { + ParticipateCode code = new ParticipateCode(participateCode); + if (voteDomainRepository.alreadyVoted(code)) { + throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); + } + } + @Transactional(readOnly = true) public VoterPollInfoResponse getVoterPollInfo(long pollId) { Poll poll = pollDomainRepository.getById(pollId); diff --git a/src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql b/src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql new file mode 100644 index 00000000..315fd427 --- /dev/null +++ b/src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql @@ -0,0 +1,5 @@ +ALTER TABLE vote + CHANGE participant_code participate_code VARCHAR (255) NOT NULL; + +ALTER TABLE vote + ADD CONSTRAINT uq_vote_poll_participate UNIQUE (poll_id, participate_code); From 28be904d9b39e0ea0da0ca49bbc61418b1a5b1ca Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 09:48:21 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20controller=EC=97=90=20service=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../debatetimer/controller/poll/VoteController.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/debatetimer/controller/poll/VoteController.java b/src/main/java/com/debatetimer/controller/poll/VoteController.java index 8351e6ca..18c309a7 100644 --- a/src/main/java/com/debatetimer/controller/poll/VoteController.java +++ b/src/main/java/com/debatetimer/controller/poll/VoteController.java @@ -3,7 +3,9 @@ import com.debatetimer.dto.poll.request.VoteRequest; import com.debatetimer.dto.poll.response.VoteCreateResponse; import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.service.poll.VoteService; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -13,13 +15,15 @@ import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor public class VoteController { + private final VoteService voteService; + @GetMapping("/api/polls/{pollId}/votes") @ResponseStatus(HttpStatus.OK) public VoterPollInfoResponse getVotersPollInfo(@PathVariable int pollId) { - - return null; + return voteService.getVoterPollInfo(pollId); } @PostMapping("/api/polls/{pollId}/votes") @@ -28,7 +32,6 @@ public VoteCreateResponse votePoll( @PathVariable int pollId, @RequestBody @Valid VoteRequest voteRequest ) { - - return null; + return voteService.vote(pollId, voteRequest); } } From c9d3d2d0d3fb8b86dedd793dc760d08313182ab4 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:06:37 +0900 Subject: [PATCH 05/20] =?UTF-8?q?test:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/poll/Poll.java | 4 + .../debatetimer/domain/poll/PollStatus.java | 4 + .../poll/VoteDomainRepository.java | 2 +- .../exception/errorcode/ClientErrorCode.java | 1 + .../repository/poll/VoteRepository.java | 3 +- .../debatetimer/service/poll/VoteService.java | 8 ++ .../controller/poll/VoteControllerTest.java | 115 ++++++++++++++++++ .../debatetimer/fixture/VoteGenerator.java | 6 +- 8 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java diff --git a/src/main/java/com/debatetimer/domain/poll/Poll.java b/src/main/java/com/debatetimer/domain/poll/Poll.java index fd1c15a5..b3b23663 100644 --- a/src/main/java/com/debatetimer/domain/poll/Poll.java +++ b/src/main/java/com/debatetimer/domain/poll/Poll.java @@ -26,4 +26,8 @@ public Poll(Long id, long tableId, long memberId, PollStatus status, String prosTeamName, String consTeamName, String agenda) { this(id, tableId, memberId, status, new TeamName(prosTeamName), new TeamName(consTeamName), new Agenda(agenda)); } + + public boolean isProgress() { + return status.isProgress(); + } } diff --git a/src/main/java/com/debatetimer/domain/poll/PollStatus.java b/src/main/java/com/debatetimer/domain/poll/PollStatus.java index f2d0ebad..98379b68 100644 --- a/src/main/java/com/debatetimer/domain/poll/PollStatus.java +++ b/src/main/java/com/debatetimer/domain/poll/PollStatus.java @@ -5,4 +5,8 @@ public enum PollStatus { PROGRESS, DONE, ; + + public boolean isProgress() { + return this == PROGRESS; + } } diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index 0e0960ef..f74d16e9 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -35,7 +35,7 @@ private VoteInfo countVotes(long pollId, List voteEntities) { } public boolean alreadyVoted(ParticipateCode code) { - return voteRepository.existsByParticipantCode(code); + return voteRepository.existsByParticipateCode(code.getValue()); } public Vote vote(Vote vote) { diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 3a57e7de..695cbb0c 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -50,6 +50,7 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_POLL_PARTICIPANT_NAME(HttpStatus.BAD_REQUEST, "잘못된 투표자 이름입니다"), INVALID_POLL_PARTICIPANT_CODE(HttpStatus.BAD_REQUEST, "잘못된 투표참여 코드입니다"), + ALREADY_DONE_POLL(HttpStatus.BAD_REQUEST, "이미 완료된 투표 입니다"), ALREADY_VOTED_PARTICIPANT(HttpStatus.BAD_REQUEST, "이미 참여한 투표자 입니다"), TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), diff --git a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java index 29573722..571f0d14 100644 --- a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java @@ -1,6 +1,5 @@ package com.debatetimer.repository.poll; -import com.debatetimer.domain.poll.ParticipateCode; import com.debatetimer.entity.poll.VoteEntity; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,5 +8,5 @@ public interface VoteRepository extends JpaRepository { List findAllByPollId(long pollId); - boolean existsByParticipantCode(ParticipateCode code); + boolean existsByParticipateCode(String participateCode); } diff --git a/src/main/java/com/debatetimer/service/poll/VoteService.java b/src/main/java/com/debatetimer/service/poll/VoteService.java index 2eccb4cd..924e081b 100644 --- a/src/main/java/com/debatetimer/service/poll/VoteService.java +++ b/src/main/java/com/debatetimer/service/poll/VoteService.java @@ -25,12 +25,20 @@ public class VoteService { @Transactional public VoteCreateResponse vote(long pollId, VoteRequest voteRequest) { + validateProgressPoll(pollId); validateAlreadyVoted(voteRequest.participateCode()); Vote vote = new Vote(pollId, voteRequest.team(), voteRequest.name(), voteRequest.participateCode()); Vote savedVote = voteDomainRepository.vote(vote); return new VoteCreateResponse(savedVote); } + private void validateProgressPoll(long pollId) { + Poll poll = pollDomainRepository.getById(pollId); + if (!poll.isProgress()) { + throw new DTClientErrorException(ClientErrorCode.ALREADY_DONE_POLL); + } + } + private void validateAlreadyVoted(String participateCode) { ParticipateCode code = new ParticipateCode(participateCode); if (voteDomainRepository.alreadyVoted(code)) { diff --git a/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java new file mode 100644 index 00000000..94ee27a4 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java @@ -0,0 +1,115 @@ +package com.debatetimer.controller.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import io.restassured.http.ContentType; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class VoteControllerTest extends BaseControllerTest { + + @Nested + class GetVotersPollInfo { + + @Test + void 투표자가_선거정보를_조회할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + + VoterPollInfoResponse response = given() + .contentType(ContentType.JSON) + .pathParam("pollId", pollEntity.getId()) + .when().get("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(VoterPollInfoResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(pollEntity.getId()), + () -> assertThat(response.prosTeamName()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(response.consTeamName()).isEqualTo(pollEntity.getConsTeamName()), + () -> assertThat(response.status()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(response.totalCount()).isEqualTo(3L), + () -> assertThat(response.participateCode()).isNotBlank(), + () -> assertThat(response.prosCount()).isEqualTo(2L), + () -> assertThat(response.consCount()).isEqualTo(1L) + ); + } + } + + @Nested + class VotePoll { + + @Test + void 진행_중인_선거에_최초로_투표_할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + String participatecode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + VoteCreateResponse response = given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.CREATED.value()) + .extract().as(VoteCreateResponse.class); + + assertAll( + () -> assertThat(response.name()).isEqualTo(voteRequest.name()), + () -> assertThat(response.participantCode()).isEqualTo(voteRequest.participateCode()), + () -> assertThat(response.team()).isEqualTo(voteRequest.team()) + ); + } + + @Test + void 이미_참여한_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + String participatecode = UUID.randomUUID().toString(); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participatecode); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void 끝난_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity alreadyDonePoll = pollGenerator.generate(table, PollStatus.DONE); + String participatecode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", alreadyDonePoll.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + } +} diff --git a/src/test/java/com/debatetimer/fixture/VoteGenerator.java b/src/test/java/com/debatetimer/fixture/VoteGenerator.java index 9efdd80c..ed97b1f1 100644 --- a/src/test/java/com/debatetimer/fixture/VoteGenerator.java +++ b/src/test/java/com/debatetimer/fixture/VoteGenerator.java @@ -17,7 +17,11 @@ public VoteGenerator(VoteRepository voteRepository) { } public VoteEntity generate(PollEntity pollEntity, VoteTeam team, String name) { - VoteEntity vote = new VoteEntity(null, pollEntity, team, name, UUID.randomUUID().toString()); + return generate(pollEntity, team, name, UUID.randomUUID().toString()); + } + + public VoteEntity generate(PollEntity pollEntity, VoteTeam team, String name, String code) { + VoteEntity vote = new VoteEntity(null, pollEntity, team, name, code); return voteRepository.save(vote); } } From d9f165b755a07f5d81b9b85d803a2a73bf6a5d17 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:12:11 +0900 Subject: [PATCH 06/20] =?UTF-8?q?test:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/poll/VoteServiceTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/test/java/com/debatetimer/service/poll/VoteServiceTest.java diff --git a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java new file mode 100644 index 00000000..30a14abf --- /dev/null +++ b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java @@ -0,0 +1,103 @@ +package com.debatetimer.service.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.service.BaseServiceTest; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class VoteServiceTest extends BaseServiceTest { + + @Autowired + private VoteService voteService; + + @Nested + class Vote { + + @Test + void 진행_중인_선거에_최초로_투표_할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + String participatecode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + VoteCreateResponse response = voteService.vote(pollEntity.getId(), voteRequest); + + assertAll( + () -> assertThat(response.name()).isEqualTo(voteRequest.name()), + () -> assertThat(response.participantCode()).isEqualTo(voteRequest.participateCode()), + () -> assertThat(response.team()).isEqualTo(voteRequest.team()) + ); + } + + @Test + void 이미_참여한_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + String participatecode = UUID.randomUUID().toString(); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participatecode); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + assertThatThrownBy(() -> voteService.vote(pollEntity.getId(), voteRequest)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.ALREADY_VOTED_PARTICIPANT.getMessage()); + } + + @Test + void 끝난_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity alreadyDonePoll = pollGenerator.generate(table, PollStatus.DONE); + String participatecode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + assertThatThrownBy(() -> voteService.vote(alreadyDonePoll.getId(), voteRequest)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.ALREADY_DONE_POLL.getMessage()); + } + } + + @Nested + class GetVoterPollInfo { + + @Test + void 투표자가_선거정보를_조회할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + + VoterPollInfoResponse response = voteService.getVoterPollInfo(pollEntity.getId()); + + assertAll( + () -> assertThat(response.id()).isEqualTo(pollEntity.getId()), + () -> assertThat(response.prosTeamName()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(response.consTeamName()).isEqualTo(pollEntity.getConsTeamName()), + () -> assertThat(response.status()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(response.totalCount()).isEqualTo(3L), + () -> assertThat(response.participateCode()).isNotBlank(), + () -> assertThat(response.prosCount()).isEqualTo(2L), + () -> assertThat(response.consCount()).isEqualTo(1L) + ); + } + } +} From 1d64f4e11adb7ac0528146832347de7352097e1b Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:16:55 +0900 Subject: [PATCH 07/20] =?UTF-8?q?test:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../service/poll/VoteServiceTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java index 30a14abf..8951bdf8 100644 --- a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java +++ b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java @@ -14,6 +14,7 @@ import com.debatetimer.entity.poll.PollEntity; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.poll.VoteRepository; import com.debatetimer.service.BaseServiceTest; import java.util.UUID; import org.junit.jupiter.api.Nested; @@ -25,6 +26,9 @@ class VoteServiceTest extends BaseServiceTest { @Autowired private VoteService voteService; + @Autowired + private VoteRepository voteRepository; + @Nested class Vote { @@ -60,6 +64,20 @@ class Vote { .hasMessage(ClientErrorCode.ALREADY_VOTED_PARTICIPANT.getMessage()); } + @Test + void 투표_동시성_이슈에_단일_표만_유효하게_취급한다() throws InterruptedException { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + String participatecode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + + runAtSameTime(10, () -> voteService.vote(pollEntity.getId(), voteRequest)); + + long voteCount = voteRepository.count(); + assertThat(voteCount).isEqualTo(1); + } + @Test void 끝난_선거에_투표_할_수_없다() { Member member = memberGenerator.generate("email@email.com"); From 8de74f94f91014c0e3e1167046bb9b7d3256f8df Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:17:38 +0900 Subject: [PATCH 08/20] =?UTF-8?q?chore:=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{poll => table}/CustomizeTableDomainRepository.java | 2 +- src/main/java/com/debatetimer/service/poll/PollService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/com/debatetimer/domainrepository/{poll => table}/CustomizeTableDomainRepository.java (95%) diff --git a/src/main/java/com/debatetimer/domainrepository/poll/CustomizeTableDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/table/CustomizeTableDomainRepository.java similarity index 95% rename from src/main/java/com/debatetimer/domainrepository/poll/CustomizeTableDomainRepository.java rename to src/main/java/com/debatetimer/domainrepository/table/CustomizeTableDomainRepository.java index 850c53bd..f48ffc33 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/CustomizeTableDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/table/CustomizeTableDomainRepository.java @@ -1,4 +1,4 @@ -package com.debatetimer.domainrepository.poll; +package com.debatetimer.domainrepository.table; import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; diff --git a/src/main/java/com/debatetimer/service/poll/PollService.java b/src/main/java/com/debatetimer/service/poll/PollService.java index a6409fdb..7d21bffb 100644 --- a/src/main/java/com/debatetimer/service/poll/PollService.java +++ b/src/main/java/com/debatetimer/service/poll/PollService.java @@ -4,9 +4,9 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.poll.Poll; import com.debatetimer.domain.poll.VoteInfo; -import com.debatetimer.domainrepository.poll.CustomizeTableDomainRepository; import com.debatetimer.domainrepository.poll.PollDomainRepository; import com.debatetimer.domainrepository.poll.VoteDomainRepository; +import com.debatetimer.domainrepository.table.CustomizeTableDomainRepository; import com.debatetimer.dto.poll.response.PollCreateResponse; import com.debatetimer.dto.poll.response.PollInfoResponse; import lombok.RequiredArgsConstructor; From a2e35964be2357d4d6720b31f894858e62b69aea Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:27:15 +0900 Subject: [PATCH 09/20] =?UTF-8?q?chore:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A4=80=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EC=A0=84=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../poll/VoteDomainRepository.java | 19 +++++++++++++---- .../util/RepositoryErrorDecoder.java | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index f74d16e9..0849a306 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -6,12 +6,16 @@ import com.debatetimer.domain.poll.VoteTeam; import com.debatetimer.entity.poll.PollEntity; import com.debatetimer.entity.poll.VoteEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.repository.poll.PollRepository; import com.debatetimer.repository.poll.VoteRepository; +import com.debatetimer.repository.util.RepositoryErrorDecoder; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Repository; @Repository @@ -39,9 +43,16 @@ public boolean alreadyVoted(ParticipateCode code) { } public Vote vote(Vote vote) { - PollEntity pollEntity = pollRepository.getById(vote.getPollId()); - VoteEntity voteEntity = new VoteEntity(vote, pollEntity); - return voteRepository.save(voteEntity) - .toDomain(); + try { + PollEntity pollEntity = pollRepository.getById(vote.getPollId()); + VoteEntity voteEntity = new VoteEntity(vote, pollEntity); + return voteRepository.save(voteEntity) + .toDomain(); + } catch (DataIntegrityViolationException exception) { + if (RepositoryErrorDecoder.isUniqueConstraintViolation(exception)) { + throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); + } + throw exception; + } } } diff --git a/src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java b/src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java new file mode 100644 index 00000000..9b756324 --- /dev/null +++ b/src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java @@ -0,0 +1,21 @@ +package com.debatetimer.repository.util; + +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; + +public class RepositoryErrorDecoder { + + private static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"; + + public static boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof ConstraintViolationException cve) { + String sqlState = cve.getSQLException().getSQLState(); + return UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE.equals(sqlState); + } + cause = cause.getCause(); + } + return false; + } +} From e2b01b5a7606d501963ca6dac04d5d3400474625 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:46:22 +0900 Subject: [PATCH 10/20] =?UTF-8?q?test:=20domainRepositoryTest=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 --- .../poll/VoteDomainRepository.java | 4 +- .../repository/poll/VoteRepository.java | 2 +- .../debatetimer/service/poll/VoteService.java | 6 +- .../poll/VoteDomainRepositoryTest.java | 62 +++++++++++++++++++ .../service/poll/VoteServiceTest.java | 20 +++--- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index 0849a306..901ed8b1 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -38,8 +38,8 @@ private VoteInfo countVotes(long pollId, List voteEntities) { return new VoteInfo(pollId, prosCount, consCount); } - public boolean alreadyVoted(ParticipateCode code) { - return voteRepository.existsByParticipateCode(code.getValue()); + public boolean alreadyVoted(long pollId, ParticipateCode code) { + return voteRepository.existsByPollIdAndParticipateCode(pollId, code.getValue()); } public Vote vote(Vote vote) { diff --git a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java index 571f0d14..5f211efd 100644 --- a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java @@ -8,5 +8,5 @@ public interface VoteRepository extends JpaRepository { List findAllByPollId(long pollId); - boolean existsByParticipateCode(String participateCode); + boolean existsByPollIdAndParticipateCode(long pollId, String participateCode); } diff --git a/src/main/java/com/debatetimer/service/poll/VoteService.java b/src/main/java/com/debatetimer/service/poll/VoteService.java index 924e081b..9d7cb8d4 100644 --- a/src/main/java/com/debatetimer/service/poll/VoteService.java +++ b/src/main/java/com/debatetimer/service/poll/VoteService.java @@ -26,7 +26,7 @@ public class VoteService { @Transactional public VoteCreateResponse vote(long pollId, VoteRequest voteRequest) { validateProgressPoll(pollId); - validateAlreadyVoted(voteRequest.participateCode()); + validateAlreadyVoted(pollId, voteRequest.participateCode()); Vote vote = new Vote(pollId, voteRequest.team(), voteRequest.name(), voteRequest.participateCode()); Vote savedVote = voteDomainRepository.vote(vote); return new VoteCreateResponse(savedVote); @@ -39,9 +39,9 @@ private void validateProgressPoll(long pollId) { } } - private void validateAlreadyVoted(String participateCode) { + private void validateAlreadyVoted(long pollId, String participateCode) { ParticipateCode code = new ParticipateCode(participateCode); - if (voteDomainRepository.alreadyVoted(code)) { + if (voteDomainRepository.alreadyVoted(pollId, code)) { throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); } } diff --git a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java index 068e782e..ad47c5a8 100644 --- a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java +++ b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java @@ -1,15 +1,21 @@ package com.debatetimer.domainrepository.poll; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.ParticipateCode; import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.Vote; import com.debatetimer.domain.poll.VoteInfo; import com.debatetimer.domain.poll.VoteTeam; import com.debatetimer.domainrepository.BaseDomainRepositoryTest; import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.UUID; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -42,4 +48,60 @@ class GetVoteInfo { } } + @Nested + class AlreadyVoted { + + @Test + void 이미_참여한_투표인지_알_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity alreadyParticipatedPoll = pollGenerator.generate(table, PollStatus.PROGRESS); + PollEntity notYetParticipatedPoll = pollGenerator.generate(table, PollStatus.PROGRESS); + ParticipateCode participateCode = new ParticipateCode(UUID.randomUUID().toString()); + voteGenerator.generate(alreadyParticipatedPoll, VoteTeam.PROS, "콜리", participateCode.getValue()); + + boolean participated = voteDomainRepository.alreadyVoted(alreadyParticipatedPoll.getId(), participateCode); + boolean notYetParticipated = voteDomainRepository.alreadyVoted(notYetParticipatedPoll.getId(), + participateCode); + + assertAll( + () -> assertThat(participated).isTrue(), + () -> assertThat(notYetParticipated).isFalse() + ); + } + } + + @Nested + class VoteTest { + + @Test + void 투표할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + Vote vote = new Vote(pollEntity.getId(), VoteTeam.PROS, "콜리", UUID.randomUUID().toString()); + + Vote savedVote = voteDomainRepository.vote(vote); + + assertAll( + () -> assertThat(savedVote.getName().getValue()).isEqualTo(vote.getName().getValue()), + () -> assertThat(savedVote.getCode().getValue()).isEqualTo(vote.getCode().getValue()), + () -> assertThat(savedVote.getTeam()).isEqualTo(vote.getTeam()) + ); + } + + @Test + void 중복_투표할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + String participateCode = UUID.randomUUID().toString(); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participateCode); + Vote vote = new Vote(pollEntity.getId(), VoteTeam.PROS, "콜리", participateCode); + + assertThatThrownBy(() -> voteDomainRepository.vote(vote)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.ALREADY_VOTED_PARTICIPANT.getMessage()); + } + } } diff --git a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java index 8951bdf8..dcc28c90 100644 --- a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java +++ b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java @@ -38,8 +38,8 @@ class Vote { CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); - String participatecode = UUID.randomUUID().toString(); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + String participateCode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); VoteCreateResponse response = voteService.vote(pollEntity.getId(), voteRequest); @@ -55,9 +55,9 @@ class Vote { Member member = memberGenerator.generate("email@email.com"); CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); - String participatecode = UUID.randomUUID().toString(); - voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participatecode); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + String participateCode = UUID.randomUUID().toString(); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participateCode); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); assertThatThrownBy(() -> voteService.vote(pollEntity.getId(), voteRequest)) .isInstanceOf(DTClientErrorException.class) @@ -69,10 +69,10 @@ class Vote { Member member = memberGenerator.generate("email@email.com"); CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); - String participatecode = UUID.randomUUID().toString(); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + String participateCode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); - runAtSameTime(10, () -> voteService.vote(pollEntity.getId(), voteRequest)); + runAtSameTime(2, () -> voteService.vote(pollEntity.getId(), voteRequest)); long voteCount = voteRepository.count(); assertThat(voteCount).isEqualTo(1); @@ -83,8 +83,8 @@ class Vote { Member member = memberGenerator.generate("email@email.com"); CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity alreadyDonePoll = pollGenerator.generate(table, PollStatus.DONE); - String participatecode = UUID.randomUUID().toString(); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + String participateCode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); assertThatThrownBy(() -> voteService.vote(alreadyDonePoll.getId(), voteRequest)) .isInstanceOf(DTClientErrorException.class) From 57bcb330490c2cbfff1b4432034bcf7d7514c6a9 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 10:55:01 +0900 Subject: [PATCH 11/20] =?UTF-8?q?test:=20=EC=97=90=EB=9F=AC=20=EB=94=94?= =?UTF-8?q?=EC=BD=94=EB=8D=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/RepositoryErrorDecoderTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java diff --git a/src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java b/src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java new file mode 100644 index 00000000..6afff6f0 --- /dev/null +++ b/src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java @@ -0,0 +1,42 @@ +package com.debatetimer.repository.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.SQLException; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataIntegrityViolationException; + +class RepositoryErrorDecoderTest { + + @Nested + class isUniqueError { + + @Test + void 유니크_제약조건_에러를_판단할_수_있다() { + SQLException uniqueError = new SQLException("유니크 에러", "23505"); + ConstraintViolationException uniqueViolation = new ConstraintViolationException("유니크 에러", uniqueError, + "vote_poll_id_participate_code"); + DataIntegrityViolationException uniqueException = new DataIntegrityViolationException("유니크 에러", + uniqueViolation); + + boolean isUniqueError = RepositoryErrorDecoder.isUniqueConstraintViolation(uniqueException); + + assertThat(isUniqueError).isTrue(); + } + + @Test + void 유니크_제약조건_에러가_아님을_판단한다() { + SQLException notUniqueError = new SQLException("다른 에러", "23000"); + ConstraintViolationException notUniqueViolation = new ConstraintViolationException("기타 에러", notUniqueError, + "some_constraint"); + DataIntegrityViolationException extraException = new DataIntegrityViolationException("에러", + notUniqueViolation); + + boolean isNotUniqueError = RepositoryErrorDecoder.isUniqueConstraintViolation(extraException); + + assertThat(isNotUniqueError).isFalse(); + } + } +} From f65c9ed2f0adb2ac340e03567c77f9e20984f2d4 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 26 Jul 2025 11:13:17 +0900 Subject: [PATCH 12/20] =?UTF-8?q?test:=20=EB=AC=B8=EC=84=9C=ED=99=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/poll/response/VoteCreateResponse.java | 2 +- .../controller/BaseDocumentTest.java | 4 + .../controller/poll/VoteControllerTest.java | 2 +- .../controller/poll/VoteDocumentTest.java | 156 ++++++++++++++++++ .../service/poll/VoteServiceTest.java | 2 +- 5 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java index cdbe4f23..903da0ff 100644 --- a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java +++ b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java @@ -8,7 +8,7 @@ public record VoteCreateResponse( long id, @NotBlank String name, - @NotBlank String participantCode, + @NotBlank String participateCode, @NotNull VoteTeam team ) { diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index 40c80cc1..0a74a3cc 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -13,6 +13,7 @@ import com.debatetimer.service.customize.CustomizeService; import com.debatetimer.service.member.MemberService; import com.debatetimer.service.poll.PollService; +import com.debatetimer.service.poll.VoteService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; @@ -66,6 +67,9 @@ public abstract class BaseDocumentTest { @MockitoBean protected PollService pollService; + @MockitoBean + protected VoteService voteService; + @MockitoBean protected AuthManager authManager; diff --git a/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java index 94ee27a4..b5343626 100644 --- a/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java +++ b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java @@ -74,7 +74,7 @@ class VotePoll { assertAll( () -> assertThat(response.name()).isEqualTo(voteRequest.name()), - () -> assertThat(response.participantCode()).isEqualTo(voteRequest.participateCode()), + () -> assertThat(response.participateCode()).isEqualTo(voteRequest.participateCode()), () -> assertThat(response.team()).isEqualTo(voteRequest.team()) ); } diff --git a/src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java b/src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java new file mode 100644 index 00000000..998c6989 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java @@ -0,0 +1,156 @@ +package com.debatetimer.controller.poll; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import com.debatetimer.controller.BaseDocumentTest; +import com.debatetimer.controller.RestDocumentationRequest; +import com.debatetimer.controller.RestDocumentationResponse; +import com.debatetimer.controller.Tag; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import io.restassured.http.ContentType; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class VoteDocumentTest extends BaseDocumentTest { + + @Nested + class GetVotersPollInfo { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("투표자 - 선거 정보 조회") + .pathParameter( + parameterWithName("pollId").description("선거 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("선거 ID"), + fieldWithPath("status").type(STRING).description("선거 상태 - 진행중 : PROGRESS, 완료 : DONE"), + fieldWithPath("prosTeamName").type(STRING).description("찬성측 팀 이름"), + fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름"), + fieldWithPath("participateCode").type(STRING).description("참여 코드"), + fieldWithPath("totalCount").type(NUMBER).description("전체 투표 수"), + fieldWithPath("prosCount").type(NUMBER).description("찬성 투표 수"), + fieldWithPath("consCount").type(NUMBER).description("반대 투표 수") + ); + + @Test + void 투표자_선거_정보_조회() { + VoterPollInfoResponse response = new VoterPollInfoResponse( + 1L, + PollStatus.PROGRESS, + "찬성", + "반대", + UUID.randomUUID().toString(), + 3L, + 2L, + 1L + ); + doReturn(response).when(voteService).getVoterPollInfo(anyLong()); + + var document = document("vote/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .pathParam("pollId", 1l) + .when().get("/api/polls/{pollId}/votes") + .then().statusCode(200); + } + } + + @Nested + class VotePoll { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("투표자 - 선거 투표") + .pathParameter( + parameterWithName("pollId").description("선거 ID") + ) + .requestBodyField( + fieldWithPath("name").type(STRING).description("투표자 이름"), + fieldWithPath("participateCode").type(STRING).description("투표 참여 코드"), + fieldWithPath("team").type(STRING).description("투표 팀") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("투표 ID"), + fieldWithPath("name").type(STRING).description("투표자 이름"), + fieldWithPath("participateCode").type(STRING).description("투표 참여 코드"), + fieldWithPath("team").type(STRING).description("투표 팀") + ); + + @Test + void 투표자_선거_정보_조회() { + VoteRequest voteRequest = new VoteRequest("콜리", UUID.randomUUID().toString(), VoteTeam.PROS); + VoteCreateResponse response = new VoteCreateResponse( + 1L, + voteRequest.name(), + voteRequest.participateCode(), + voteRequest.team() + ); + doReturn(response).when(voteService).vote(anyLong(), any()); + + var document = document("vote/post", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", 1l) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(201); + } + + @EnumSource( + value = ClientErrorCode.class, + names = { + "ALREADY_DONE_POLL", + "ALREADY_VOTED_PARTICIPANT", + "INVALID_POLL_PARTICIPANT_CODE", + "INVALID_POLL_PARTICIPANT_NAME" + } + ) + @ParameterizedTest + void 투표자_투표_실패(ClientErrorCode clientErrorCode) { + VoteRequest voteRequest = new VoteRequest("콜리", UUID.randomUUID().toString(), VoteTeam.PROS); + doThrow(new DTClientErrorException(clientErrorCode)).when(voteService).vote(anyLong(), any()); + + var document = document("vote/post", clientErrorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", 1l) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(clientErrorCode.getStatus().value()); + + } + } +} diff --git a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java index dcc28c90..52626c73 100644 --- a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java +++ b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java @@ -45,7 +45,7 @@ class Vote { assertAll( () -> assertThat(response.name()).isEqualTo(voteRequest.name()), - () -> assertThat(response.participantCode()).isEqualTo(voteRequest.participateCode()), + () -> assertThat(response.participateCode()).isEqualTo(voteRequest.participateCode()), () -> assertThat(response.team()).isEqualTo(voteRequest.team()) ); } From 036010e22014b6a83a7768b8222389ccf2d682b7 Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 09:36:42 +0900 Subject: [PATCH 13/20] =?UTF-8?q?refactor:=20type=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EC=84=B1=EC=97=90=20=EB=A7=9E=EB=8F=84=EB=A1=9D=20long=20?= =?UTF-8?q?=ED=98=95=ED=83=9C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/debatetimer/controller/poll/VoteController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/debatetimer/controller/poll/VoteController.java b/src/main/java/com/debatetimer/controller/poll/VoteController.java index 18c309a7..64f53365 100644 --- a/src/main/java/com/debatetimer/controller/poll/VoteController.java +++ b/src/main/java/com/debatetimer/controller/poll/VoteController.java @@ -22,14 +22,14 @@ public class VoteController { @GetMapping("/api/polls/{pollId}/votes") @ResponseStatus(HttpStatus.OK) - public VoterPollInfoResponse getVotersPollInfo(@PathVariable int pollId) { + public VoterPollInfoResponse getVotersPollInfo(@PathVariable long pollId) { return voteService.getVoterPollInfo(pollId); } @PostMapping("/api/polls/{pollId}/votes") @ResponseStatus(HttpStatus.CREATED) public VoteCreateResponse votePoll( - @PathVariable int pollId, + @PathVariable long pollId, @RequestBody @Valid VoteRequest voteRequest ) { return voteService.vote(pollId, voteRequest); From 2ae97ca793e8debf3a4bc155bf678f6a6f9723ac Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 09:38:40 +0900 Subject: [PATCH 14/20] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20validation=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../debatetimer/dto/poll/response/VoteCreateResponse.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java index 903da0ff..07bf2f2a 100644 --- a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java +++ b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java @@ -2,14 +2,12 @@ import com.debatetimer.domain.poll.Vote; import com.debatetimer.domain.poll.VoteTeam; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; public record VoteCreateResponse( long id, - @NotBlank String name, - @NotBlank String participateCode, - @NotNull VoteTeam team + String name, + String participateCode, + VoteTeam team ) { public VoteCreateResponse(Vote vote) { From da215be8d327cf3a03f971589dbda95e2c04d245 Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 09:42:13 +0900 Subject: [PATCH 15/20] =?UTF-8?q?rename:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=EC=97=90=20?= =?UTF-8?q?=EC=95=8C=EB=A7=9E=EC=9D=80=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domainrepository/poll/VoteDomainRepository.java | 4 ++-- .../com/debatetimer/service/poll/VoteService.java | 4 ++-- .../poll/VoteDomainRepositoryTest.java | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index 901ed8b1..e674e140 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -38,11 +38,11 @@ private VoteInfo countVotes(long pollId, List voteEntities) { return new VoteInfo(pollId, prosCount, consCount); } - public boolean alreadyVoted(long pollId, ParticipateCode code) { + public boolean isExists(long pollId, ParticipateCode code) { return voteRepository.existsByPollIdAndParticipateCode(pollId, code.getValue()); } - public Vote vote(Vote vote) { + public Vote save(Vote vote) { try { PollEntity pollEntity = pollRepository.getById(vote.getPollId()); VoteEntity voteEntity = new VoteEntity(vote, pollEntity); diff --git a/src/main/java/com/debatetimer/service/poll/VoteService.java b/src/main/java/com/debatetimer/service/poll/VoteService.java index 9d7cb8d4..8c485907 100644 --- a/src/main/java/com/debatetimer/service/poll/VoteService.java +++ b/src/main/java/com/debatetimer/service/poll/VoteService.java @@ -28,7 +28,7 @@ public VoteCreateResponse vote(long pollId, VoteRequest voteRequest) { validateProgressPoll(pollId); validateAlreadyVoted(pollId, voteRequest.participateCode()); Vote vote = new Vote(pollId, voteRequest.team(), voteRequest.name(), voteRequest.participateCode()); - Vote savedVote = voteDomainRepository.vote(vote); + Vote savedVote = voteDomainRepository.save(vote); return new VoteCreateResponse(savedVote); } @@ -41,7 +41,7 @@ private void validateProgressPoll(long pollId) { private void validateAlreadyVoted(long pollId, String participateCode) { ParticipateCode code = new ParticipateCode(participateCode); - if (voteDomainRepository.alreadyVoted(pollId, code)) { + if (voteDomainRepository.isExists(pollId, code)) { throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); } } diff --git a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java index ad47c5a8..95cde4c5 100644 --- a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java +++ b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java @@ -49,7 +49,7 @@ class GetVoteInfo { } @Nested - class AlreadyVoted { + class isExists { @Test void 이미_참여한_투표인지_알_수_있다() { @@ -60,8 +60,8 @@ class AlreadyVoted { ParticipateCode participateCode = new ParticipateCode(UUID.randomUUID().toString()); voteGenerator.generate(alreadyParticipatedPoll, VoteTeam.PROS, "콜리", participateCode.getValue()); - boolean participated = voteDomainRepository.alreadyVoted(alreadyParticipatedPoll.getId(), participateCode); - boolean notYetParticipated = voteDomainRepository.alreadyVoted(notYetParticipatedPoll.getId(), + boolean participated = voteDomainRepository.isExists(alreadyParticipatedPoll.getId(), participateCode); + boolean notYetParticipated = voteDomainRepository.isExists(notYetParticipatedPoll.getId(), participateCode); assertAll( @@ -72,7 +72,7 @@ class AlreadyVoted { } @Nested - class VoteTest { + class Save { @Test void 투표할_수_있다() { @@ -81,7 +81,7 @@ class VoteTest { PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); Vote vote = new Vote(pollEntity.getId(), VoteTeam.PROS, "콜리", UUID.randomUUID().toString()); - Vote savedVote = voteDomainRepository.vote(vote); + Vote savedVote = voteDomainRepository.save(vote); assertAll( () -> assertThat(savedVote.getName().getValue()).isEqualTo(vote.getName().getValue()), @@ -99,7 +99,7 @@ class VoteTest { voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participateCode); Vote vote = new Vote(pollEntity.getId(), VoteTeam.PROS, "콜리", participateCode); - assertThatThrownBy(() -> voteDomainRepository.vote(vote)) + assertThatThrownBy(() -> voteDomainRepository.save(vote)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.ALREADY_VOTED_PARTICIPANT.getMessage()); } From ac9849c354a7d47eb2e6566f371df64e61a70c78 Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 10:19:22 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor:=20decoder=20interface=20?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/ErrorDecoderConfig.java | 33 ++++++++++++ .../poll/VoteDomainRepository.java | 5 +- .../decoder/H2ErrorDecoder.java} | 9 ++-- .../exception/decoder/MySqlErrorDecoder.java | 27 ++++++++++ .../decoder/RepositoryErrorDecoder.java | 8 +++ .../poll/VoteDomainRepositoryTest.java | 2 +- .../decoder/H2ErrorDecoderTest.java} | 19 +++++-- .../decoder/MySqlErrorDecoderTest.java | 54 +++++++++++++++++++ 8 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/debatetimer/config/ErrorDecoderConfig.java rename src/main/java/com/debatetimer/{repository/util/RepositoryErrorDecoder.java => exception/decoder/H2ErrorDecoder.java} (64%) create mode 100644 src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java create mode 100644 src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java rename src/test/java/com/debatetimer/{repository/util/RepositoryErrorDecoderTest.java => exception/decoder/H2ErrorDecoderTest.java} (74%) create mode 100644 src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java diff --git a/src/main/java/com/debatetimer/config/ErrorDecoderConfig.java b/src/main/java/com/debatetimer/config/ErrorDecoderConfig.java new file mode 100644 index 00000000..5fee2c04 --- /dev/null +++ b/src/main/java/com/debatetimer/config/ErrorDecoderConfig.java @@ -0,0 +1,33 @@ +package com.debatetimer.config; + +import com.debatetimer.exception.decoder.H2ErrorDecoder; +import com.debatetimer.exception.decoder.MySqlErrorDecoder; +import com.debatetimer.exception.decoder.RepositoryErrorDecoder; +import lombok.NoArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class ErrorDecoderConfig { + + @Profile({"dev", "prod"}) + @Configuration + public static class MySqlErrorDecoderConfig { + + @Bean + public RepositoryErrorDecoder mySqlErrorDecoder() { + return new MySqlErrorDecoder(); + } + } + + @Profile({"test", "local"}) + @Configuration + public static class H2ErrorDecoderConfig { + + @Bean + public RepositoryErrorDecoder h2ErrorDecoder() { + return new H2ErrorDecoder(); + } + } +} diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java index e674e140..dc79f01f 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -7,10 +7,10 @@ import com.debatetimer.entity.poll.PollEntity; import com.debatetimer.entity.poll.VoteEntity; import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.decoder.RepositoryErrorDecoder; import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.repository.poll.PollRepository; import com.debatetimer.repository.poll.VoteRepository; -import com.debatetimer.repository.util.RepositoryErrorDecoder; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -24,6 +24,7 @@ public class VoteDomainRepository { private final PollRepository pollRepository; private final VoteRepository voteRepository; + private final RepositoryErrorDecoder errorDecoder; public VoteInfo findVoteInfoByPollId(long pollId) { List pollVotes = voteRepository.findAllByPollId(pollId); @@ -49,7 +50,7 @@ public Vote save(Vote vote) { return voteRepository.save(voteEntity) .toDomain(); } catch (DataIntegrityViolationException exception) { - if (RepositoryErrorDecoder.isUniqueConstraintViolation(exception)) { + if (errorDecoder.isUniqueConstraintViolation(exception)) { throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); } throw exception; diff --git a/src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java similarity index 64% rename from src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java rename to src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java index 9b756324..e2ea319d 100644 --- a/src/main/java/com/debatetimer/repository/util/RepositoryErrorDecoder.java +++ b/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java @@ -1,13 +1,14 @@ -package com.debatetimer.repository.util; +package com.debatetimer.exception.decoder; import org.hibernate.exception.ConstraintViolationException; import org.springframework.dao.DataIntegrityViolationException; -public class RepositoryErrorDecoder { +public class H2ErrorDecoder implements RepositoryErrorDecoder { - private static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"; + public static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"; - public static boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { + @Override + public boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { Throwable cause = e.getCause(); while (cause != null) { if (cause instanceof ConstraintViolationException cve) { diff --git a/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java new file mode 100644 index 00000000..a203ba2d --- /dev/null +++ b/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java @@ -0,0 +1,27 @@ +package com.debatetimer.exception.decoder; + +import java.sql.SQLException; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; + +public class MySqlErrorDecoder implements RepositoryErrorDecoder { + + public static final String MYSQL_UNIQUE_VIOLATION = "23000"; + public static final int MYSQL_DUP_ERROR_CODE = 1062; + + @Override + public boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof ConstraintViolationException cve) { + SQLException sqlEx = cve.getSQLException(); + String sqlState = sqlEx.getSQLState(); + int errorCode = sqlEx.getErrorCode(); + return MYSQL_UNIQUE_VIOLATION.equals(sqlState) + && MYSQL_DUP_ERROR_CODE == errorCode; + } + cause = cause.getCause(); + } + return false; + } +} diff --git a/src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java new file mode 100644 index 00000000..8d71e834 --- /dev/null +++ b/src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java @@ -0,0 +1,8 @@ +package com.debatetimer.exception.decoder; + +import org.springframework.dao.DataIntegrityViolationException; + +public interface RepositoryErrorDecoder { + + boolean isUniqueConstraintViolation(DataIntegrityViolationException exception); +} diff --git a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java index 95cde4c5..11ed5d33 100644 --- a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java +++ b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java @@ -75,7 +75,7 @@ class isExists { class Save { @Test - void 투표할_수_있다() { + void 투표를_저장할_수_있다() { Member member = memberGenerator.generate("email@email.com"); CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); diff --git a/src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java b/src/test/java/com/debatetimer/exception/decoder/H2ErrorDecoderTest.java similarity index 74% rename from src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java rename to src/test/java/com/debatetimer/exception/decoder/H2ErrorDecoderTest.java index 6afff6f0..147ff327 100644 --- a/src/test/java/com/debatetimer/repository/util/RepositoryErrorDecoderTest.java +++ b/src/test/java/com/debatetimer/exception/decoder/H2ErrorDecoderTest.java @@ -1,27 +1,36 @@ -package com.debatetimer.repository.util; +package com.debatetimer.exception.decoder; import static org.assertj.core.api.Assertions.assertThat; import java.sql.SQLException; import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.dao.DataIntegrityViolationException; -class RepositoryErrorDecoderTest { +class H2ErrorDecoderTest { + + private H2ErrorDecoder errorDecoder; + + + @BeforeEach + void setUp() { + errorDecoder = new H2ErrorDecoder(); + } @Nested class isUniqueError { @Test void 유니크_제약조건_에러를_판단할_수_있다() { - SQLException uniqueError = new SQLException("유니크 에러", "23505"); + SQLException uniqueError = new SQLException("유니크 에러", H2ErrorDecoder.UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE); ConstraintViolationException uniqueViolation = new ConstraintViolationException("유니크 에러", uniqueError, "vote_poll_id_participate_code"); DataIntegrityViolationException uniqueException = new DataIntegrityViolationException("유니크 에러", uniqueViolation); - boolean isUniqueError = RepositoryErrorDecoder.isUniqueConstraintViolation(uniqueException); + boolean isUniqueError = errorDecoder.isUniqueConstraintViolation(uniqueException); assertThat(isUniqueError).isTrue(); } @@ -34,7 +43,7 @@ class isUniqueError { DataIntegrityViolationException extraException = new DataIntegrityViolationException("에러", notUniqueViolation); - boolean isNotUniqueError = RepositoryErrorDecoder.isUniqueConstraintViolation(extraException); + boolean isNotUniqueError = errorDecoder.isUniqueConstraintViolation(extraException); assertThat(isNotUniqueError).isFalse(); } diff --git a/src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java b/src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java new file mode 100644 index 00000000..f49d5978 --- /dev/null +++ b/src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java @@ -0,0 +1,54 @@ +package com.debatetimer.exception.decoder; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.SQLException; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataIntegrityViolationException; + +class MySqlErrorDecoderTest { + + private MySqlErrorDecoder errorDecoder; + + + @BeforeEach + void setUp() { + errorDecoder = new MySqlErrorDecoder(); + } + + @Nested + class isUniqueError { + + @Test + void 유니크_제약조건_에러를_판단할_수_있다() { + SQLException uniqueError = new SQLException("유니크 에러", + MySqlErrorDecoder.MYSQL_UNIQUE_VIOLATION, + MySqlErrorDecoder.MYSQL_DUP_ERROR_CODE + ); + ConstraintViolationException uniqueViolation = new ConstraintViolationException("유니크 에러", uniqueError, + "vote_poll_id_participate_code"); + DataIntegrityViolationException uniqueException = new DataIntegrityViolationException("유니크 에러", + uniqueViolation); + + boolean isUniqueError = errorDecoder.isUniqueConstraintViolation(uniqueException); + + assertThat(isUniqueError).isTrue(); + } + + @Test + void 유니크_제약조건_에러가_아님을_판단한다() { + SQLException notUniqueError = new SQLException("다른 에러", "32050", 1234); + ConstraintViolationException notUniqueViolation = new ConstraintViolationException("기타 에러", notUniqueError, + "some_constraint"); + DataIntegrityViolationException extraException = new DataIntegrityViolationException("에러", + notUniqueViolation); + + boolean isNotUniqueError = errorDecoder.isUniqueConstraintViolation(extraException); + + assertThat(isNotUniqueError).isFalse(); + } + } +} From 81f4b432dd6aa15544549dc2009c501a9d189d3a Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 10:23:49 +0900 Subject: [PATCH 17/20] =?UTF-8?q?refactor:=20=EB=94=94=EC=BD=94=EB=8D=94?= =?UTF-8?q?=20=EC=83=81=EC=88=98=EB=A5=BC=20protected=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/exception/decoder/H2ErrorDecoder.java | 2 +- .../com/debatetimer/exception/decoder/MySqlErrorDecoder.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java index e2ea319d..42ea2660 100644 --- a/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java +++ b/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java @@ -5,7 +5,7 @@ public class H2ErrorDecoder implements RepositoryErrorDecoder { - public static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"; + protected static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"; @Override public boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { diff --git a/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java index a203ba2d..f7ddf38c 100644 --- a/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java +++ b/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java @@ -6,8 +6,8 @@ public class MySqlErrorDecoder implements RepositoryErrorDecoder { - public static final String MYSQL_UNIQUE_VIOLATION = "23000"; - public static final int MYSQL_DUP_ERROR_CODE = 1062; + protected static final String MYSQL_UNIQUE_VIOLATION = "23000"; + protected static final int MYSQL_DUP_ERROR_CODE = 1062; @Override public boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { From 7cc9b03239410bf5c9d61fe261dbbd143a1c1e7e Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 11:51:51 +0900 Subject: [PATCH 18/20] =?UTF-8?q?test:=20null=20&=20blank=20test=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BaseControllerTest.java | 10 +++ .../controller/poll/VoteControllerTest.java | 68 +++++++++++++++++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 83d35dda..c63a61fb 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -4,10 +4,12 @@ import com.debatetimer.client.oauth.OAuthClient; import com.debatetimer.domain.customize.CustomizeBoxType; import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.poll.VoteTeam; import com.debatetimer.dto.customize.request.BellRequest; import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; +import com.debatetimer.dto.poll.request.VoteRequest; import com.debatetimer.fixture.CustomizeTableGenerator; import com.debatetimer.fixture.CustomizeTimeBoxGenerator; import com.debatetimer.fixture.HeaderGenerator; @@ -24,6 +26,7 @@ import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.specification.RequestSpecification; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -90,6 +93,13 @@ protected ArbitraryBuilder getCustomizeTableCreateR .set("table", getCustomizeTimeBoxCreateRequestBuilder().sampleList(2)); } + protected ArbitraryBuilder getVoteRequestBuilder() { + return fixtureMonkey.giveMeBuilder(VoteRequest.class) + .set("name", "콜리") + .set("team", VoteTeam.PROS) + .set("participateCode", UUID.randomUUID().toString()); + } + private ArbitraryBuilder getCustomizeTableInfoCreateRequestBuilder() { return fixtureMonkey.giveMeBuilder(CustomizeTableInfoCreateRequest.class) .set("name", "자유 테이블") diff --git a/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java index b5343626..06276c5b 100644 --- a/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java +++ b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java @@ -12,10 +12,12 @@ import com.debatetimer.dto.poll.response.VoterPollInfoResponse; import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.fixture.NullAndEmptyAndBlankSource; import io.restassured.http.ContentType; import java.util.UUID; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; import org.springframework.http.HttpStatus; class VoteControllerTest extends BaseControllerTest { @@ -61,8 +63,7 @@ class VotePoll { CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); - String participatecode = UUID.randomUUID().toString(); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + VoteRequest voteRequest = getVoteRequestBuilder().sample(); VoteCreateResponse response = given() .contentType(ContentType.JSON) @@ -79,6 +80,62 @@ class VotePoll { ); } + @ParameterizedTest + @NullAndEmptyAndBlankSource + void 투표_시_이름은_널이거나_빈_문자열일_수_없다(String invalidName) { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("name", invalidName) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void 투표_시_팀은_널일_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("team", null) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @ParameterizedTest + @NullAndEmptyAndBlankSource + void 투표_시_참여코드는_널이거나_빈_문자열일_수_없다(String participateCode) { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableGenerator.generate(member); + PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); + voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("participateCode", participateCode) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + @Test void 이미_참여한_선거에_투표_할_수_없다() { Member member = memberGenerator.generate("email@email.com"); @@ -86,7 +143,9 @@ class VotePoll { PollEntity pollEntity = pollGenerator.generate(table, PollStatus.PROGRESS); String participatecode = UUID.randomUUID().toString(); voteGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participatecode); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("participateCode", participatecode) + .sample(); given() .contentType(ContentType.JSON) @@ -101,8 +160,7 @@ class VotePoll { Member member = memberGenerator.generate("email@email.com"); CustomizeTableEntity table = customizeTableGenerator.generate(member); PollEntity alreadyDonePoll = pollGenerator.generate(table, PollStatus.DONE); - String participatecode = UUID.randomUUID().toString(); - VoteRequest voteRequest = new VoteRequest("콜리", participatecode, VoteTeam.PROS); + VoteRequest voteRequest = getVoteRequestBuilder().sample(); given() .contentType(ContentType.JSON) From 00ad2672dab773081f7a441e540e58a33ac623b0 Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 29 Jul 2025 11:54:55 +0900 Subject: [PATCH 19/20] =?UTF-8?q?refactor:=20table=EC=97=90=20timeBoxEntit?= =?UTF-8?q?y=20=EC=83=81=EC=86=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/entity/customize/CustomizeTableEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java b/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java index 88cfd3bb..bffe1179 100644 --- a/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java +++ b/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java @@ -22,7 +22,7 @@ @Getter @Table(name = "customize_table") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CustomizeTableEntity { +public class CustomizeTableEntity extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 8094397283e2d4dc39478e7a5e2b2fe09aeaf98c Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 31 Jul 2025 03:29:22 +0900 Subject: [PATCH 20/20] =?UTF-8?q?chore:=20BaseTimeEntity=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/debatetimer/domain/member/Member.java | 2 +- .../com/debatetimer/entity/{customize => }/BaseTimeEntity.java | 2 +- .../com/debatetimer/entity/customize/CustomizeTableEntity.java | 1 + src/main/java/com/debatetimer/entity/poll/PollEntity.java | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename src/main/java/com/debatetimer/entity/{customize => }/BaseTimeEntity.java (93%) diff --git a/src/main/java/com/debatetimer/domain/member/Member.java b/src/main/java/com/debatetimer/domain/member/Member.java index 9dec2246..923d17c5 100644 --- a/src/main/java/com/debatetimer/domain/member/Member.java +++ b/src/main/java/com/debatetimer/domain/member/Member.java @@ -1,6 +1,6 @@ package com.debatetimer.domain.member; -import com.debatetimer.entity.customize.BaseTimeEntity; +import com.debatetimer.entity.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/com/debatetimer/entity/customize/BaseTimeEntity.java b/src/main/java/com/debatetimer/entity/BaseTimeEntity.java similarity index 93% rename from src/main/java/com/debatetimer/entity/customize/BaseTimeEntity.java rename to src/main/java/com/debatetimer/entity/BaseTimeEntity.java index 1bca8c75..e9405201 100644 --- a/src/main/java/com/debatetimer/entity/customize/BaseTimeEntity.java +++ b/src/main/java/com/debatetimer/entity/BaseTimeEntity.java @@ -1,4 +1,4 @@ -package com.debatetimer.entity.customize; +package com.debatetimer.entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; diff --git a/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java b/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java index bffe1179..ff24334e 100644 --- a/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java +++ b/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java @@ -3,6 +3,7 @@ import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.TableType; +import com.debatetimer.entity.BaseTimeEntity; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/com/debatetimer/entity/poll/PollEntity.java b/src/main/java/com/debatetimer/entity/poll/PollEntity.java index bb4e5433..04e8ac28 100644 --- a/src/main/java/com/debatetimer/entity/poll/PollEntity.java +++ b/src/main/java/com/debatetimer/entity/poll/PollEntity.java @@ -2,7 +2,7 @@ import com.debatetimer.domain.poll.Poll; import com.debatetimer.domain.poll.PollStatus; -import com.debatetimer.entity.customize.BaseTimeEntity; +import com.debatetimer.entity.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType;