Skip to content

Commit 9c4568a

Browse files
authored
Merge pull request #360 from prgrms-web-devcourse-final-project/feat/22-post
Fix[poll]:중복투표오류해결
2 parents 6d85edb + 5b84077 commit 9c4568a

File tree

1 file changed

+42
-39
lines changed

1 file changed

+42
-39
lines changed

backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.LocalDateTime;
2222

2323
import com.ai.lawyer.global.util.AuthUtil;
24+
import org.springframework.dao.DataIntegrityViolationException;
2425

2526
@Service
2627
@Transactional
@@ -106,61 +107,63 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) {
106107
}
107108
PollOptions pollOptions = pollOptionsRepository.findById(pollItemsId)
108109
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표 항목을 찾을 수 없습니다."));
110+
if (pollOptions.getPoll() == null || !pollOptions.getPoll().getPollId().equals(pollId)) {
111+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "투표 항목이 해당 투표에 속하지 않습니다.");
112+
}
113+
109114
Member member = AuthUtil.getMemberOrThrow(memberId);
110-
// USER 또는 ADMIN만 투표 가능
111115
if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) {
112116
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다.");
113117
}
114118

115119
try {
116-
// 기존 투표 내역 조회
117-
var existingVoteOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
118-
if (existingVoteOpt.isPresent()) {
119-
PollVote existingVote = existingVoteOpt.get();
120-
if (existingVote.getPollOptions().getPollItemsId().equals(pollItemsId)) {
121-
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다.");
122-
} else {
123-
pollVoteRepository.deleteByMemberIdAndPoll_PollId(memberId, pollId);
124-
PollVote pollVote = PollVote.builder()
125-
.poll(poll)
126-
.pollOptions(pollOptions)
127-
.memberId(memberId)
128-
.build();
129-
PollVote savedVote = pollVoteRepository.save(pollVote);
130-
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
131-
return PollVoteDto.builder()
132-
.pollVoteId(savedVote.getPollVoteId())
133-
.pollId(pollId)
134-
.pollItemsId(pollItemsId)
135-
.memberId(memberId)
136-
.voteCount(voteCount)
137-
.message("투표 항목을 변경하였습니다.")
138-
.build();
139-
}
120+
// 1) 기존 투표가 있는지 조회
121+
var existingOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
122+
if (existingOpt.isPresent()) {
123+
return handleExistingVote(existingOpt.get(), pollOptions, pollId, pollItemsId, memberId);
140124
}
141-
// 기존 투표 내역이 없으면 정상 투표
142-
PollVote pollVote = PollVote.builder()
125+
// 2) 신규 투표 생성
126+
PollVote newVote = PollVote.builder()
143127
.poll(poll)
144128
.pollOptions(pollOptions)
145129
.memberId(memberId)
146130
.build();
147-
PollVote savedVote = pollVoteRepository.save(pollVote);
148-
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
149-
return PollVoteDto.builder()
150-
.pollVoteId(savedVote.getPollVoteId())
151-
.pollId(pollId)
152-
.pollItemsId(pollItemsId)
153-
.memberId(memberId)
154-
.voteCount(voteCount)
155-
.message("투표가 완료되었습니다.")
156-
.build();
157-
} catch (org.springframework.dao.DataIntegrityViolationException e) {
158-
// 동시성 문제로 인한 중복 투표 시도 (unique constraint violation)
131+
PollVote saved = pollVoteRepository.save(newVote);
132+
return buildPollVote(saved, pollId, pollItemsId, memberId, "투표가 완료되었습니다.");
133+
} catch (DataIntegrityViolationException e) {
134+
// 동시성(경합)으로 인해 이미 다른 쓰레드가 투표를 만들어 중복 제약에 걸린 경우 복구 처리
159135
log.warn("중복 투표 시도 감지 - memberId: {}, pollId: {}", memberId, pollId, e);
136+
var existingAfterOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
137+
if (existingAfterOpt.isPresent()) {
138+
return handleExistingVote(existingAfterOpt.get(), pollOptions, pollId, pollItemsId, memberId);
139+
}
160140
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다.");
161141
}
162142
}
163143

144+
private PollVoteDto handleExistingVote(PollVote existing, PollOptions targetOption,
145+
Long pollId, Long pollItemsId, Long memberId) {
146+
// 동일 항목이면 idempotent 응답
147+
if (existing.getPollOptions().getPollItemsId().equals(pollItemsId)) {
148+
return buildPollVote(existing, pollId, pollItemsId, memberId, "이미 해당 항목에 투표하셨습니다.");
149+
}
150+
existing.setPollOptions(targetOption);
151+
PollVote saved = pollVoteRepository.save(existing);
152+
return buildPollVote(saved, pollId, pollItemsId, memberId, "투표 항목을 변경하였습니다.");
153+
}
154+
155+
private PollVoteDto buildPollVote(PollVote vote, Long pollId, Long pollItemsId, Long memberId, String message) {
156+
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
157+
return PollVoteDto.builder()
158+
.pollVoteId(vote != null ? vote.getPollVoteId() : null)
159+
.pollId(pollId)
160+
.pollItemsId(pollItemsId)
161+
.memberId(memberId)
162+
.voteCount(voteCount)
163+
.message(message)
164+
.build();
165+
}
166+
164167
@Override
165168
public PollVoteDto voteByIndex(Long pollId, int index, Long memberId) {
166169
List<PollOptions> options = getPollOptions(pollId);

0 commit comments

Comments
 (0)