Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
43cba4b
feat: controller 계층 구현
coli-geonwoo Jul 26, 2025
d33dd4c
feat: service 로직 구현
coli-geonwoo Jul 26, 2025
dd6ac7c
feat: 중복 투표 검증로직 추가
coli-geonwoo Jul 26, 2025
28be904
feat: controller에 service 연결
coli-geonwoo Jul 26, 2025
c9d3d2d
test: 컨트롤러 테스트 작성
coli-geonwoo Jul 26, 2025
d9f165b
test: 서비스 테스트 작성
coli-geonwoo Jul 26, 2025
1d64f4e
test: 서비스 동시성 테스트 작성
coli-geonwoo Jul 26, 2025
8de74f9
chore: 패키지 이동
coli-geonwoo Jul 26, 2025
a2e3596
chore: 도메인 수준에 맞게 에러를 전환하도록 수정
coli-geonwoo Jul 26, 2025
e2b01b5
test: domainRepositoryTest 작성
coli-geonwoo Jul 26, 2025
57bcb33
test: 에러 디코더 테스트 추가
coli-geonwoo Jul 26, 2025
f65c9ed
test: 문서화 코드 작성
coli-geonwoo Jul 26, 2025
036010e
refactor: type 일관성에 맞도록 long 형태로 수정
coli-geonwoo Jul 29, 2025
2ae97ca
refactor: 불필요한 validation 제거
coli-geonwoo Jul 29, 2025
da215be
rename: 도메인 레포지토리를 레포지토리에 알맞은 메서드명으로 변경
coli-geonwoo Jul 29, 2025
ac9849c
refactor: decoder interface 화
coli-geonwoo Jul 29, 2025
81f4b43
refactor: 디코더 상수를 protected로 변경
coli-geonwoo Jul 29, 2025
7cc9b03
test: null & blank test 추가
coli-geonwoo Jul 29, 2025
00ad267
refactor: table에 timeBoxEntity 상속
coli-geonwoo Jul 29, 2025
8094397
chore: BaseTimeEntity 패키지 이동
coli-geonwoo Jul 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/main/java/com/debatetimer/config/ErrorDecoderConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/debatetimer/controller/poll/VoteController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 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;
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
@RequiredArgsConstructor
public class VoteController {

private final VoteService voteService;

@GetMapping("/api/polls/{pollId}/votes")
@ResponseStatus(HttpStatus.OK)
public VoterPollInfoResponse getVotersPollInfo(@PathVariable long pollId) {
return voteService.getVoterPollInfo(pollId);
}

@PostMapping("/api/polls/{pollId}/votes")
@ResponseStatus(HttpStatus.CREATED)
public VoteCreateResponse votePoll(
@PathVariable long pollId,
@RequestBody @Valid VoteRequest voteRequest
) {
return voteService.vote(pollId, voteRequest);
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/debatetimer/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/debatetimer/domain/poll/Poll.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/debatetimer/domain/poll/PollStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public enum PollStatus {
PROGRESS,
DONE,
;

public boolean isProgress() {
return this == PROGRESS;
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/debatetimer/domain/poll/Vote.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
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;
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 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
@RequiredArgsConstructor
public class VoteDomainRepository {

private final PollRepository pollRepository;
private final VoteRepository voteRepository;
private final RepositoryErrorDecoder errorDecoder;

public VoteInfo findVoteInfoByPollId(long pollId) {
List<VoteEntity> pollVotes = voteRepository.findAllByPollId(pollId);
Expand All @@ -28,4 +38,22 @@ private VoteInfo countVotes(long pollId, List<VoteEntity> voteEntities) {
long consCount = teamCount.getOrDefault(VoteTeam.CONS, 0L);
return new VoteInfo(pollId, prosCount, consCount);
}

public boolean isExists(long pollId, ParticipateCode code) {
return voteRepository.existsByPollIdAndParticipateCode(pollId, code.getValue());
}

public Vote save(Vote vote) {
try {
PollEntity pollEntity = pollRepository.getById(vote.getPollId());
VoteEntity voteEntity = new VoteEntity(vote, pollEntity);
return voteRepository.save(voteEntity)
.toDomain();
} catch (DataIntegrityViolationException exception) {
if (errorDecoder.isUniqueConstraintViolation(exception)) {
throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT);
}
throw exception;
}
}
Comment on lines +46 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.debatetimer.domainrepository.poll;
package com.debatetimer.domainrepository.table;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??? 이걸 왜 발견 못 했었지...


import com.debatetimer.domain.customize.CustomizeTable;
import com.debatetimer.domain.member.Member;
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java
Original file line number Diff line number Diff line change
@@ -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 participateCode,
@NotNull VoteTeam team
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.debatetimer.dto.poll.response;

import com.debatetimer.domain.poll.Vote;
import com.debatetimer.domain.poll.VoteTeam;

public record VoteCreateResponse(
long id,
String name,
String participateCode,
VoteTeam team
) {

public VoteCreateResponse(Vote vote) {
this(vote.getId(), vote.getName().getValue(), vote.getCode().getValue(), vote.getTeam());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +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 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()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.debatetimer.entity.customize;
package com.debatetimer.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,7 +23,7 @@
@Getter
@Table(name = "customize_table")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CustomizeTableEntity {
public class CustomizeTableEntity extends BaseTimeEntity {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseTimeEntity가 PollEntity에서도 쓰이던데 패키지 이동해주세요.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 반영 완료


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/debatetimer/entity/poll/PollEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/com/debatetimer/entity/poll/VoteEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,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;
Expand All @@ -21,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 {
Expand All @@ -43,9 +47,14 @@ public class VoteEntity {
private String name;

@NotBlank
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.debatetimer.exception.decoder;

import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;

public class H2ErrorDecoder implements RepositoryErrorDecoder {

protected static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505";

@Override
public 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;
}
}
Original file line number Diff line number Diff line change
@@ -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 {

protected static final String MYSQL_UNIQUE_VIOLATION = "23000";
protected 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;
}
}
Loading