Skip to content

Commit 77f4a90

Browse files
committed
Fix[poll]:중복투표오류해결
1 parent abf1b87 commit 77f4a90

File tree

1 file changed

+43
-39
lines changed

1 file changed

+43
-39
lines changed

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

Lines changed: 43 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,64 @@ 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만 투표 가능
115+
// enum 비교로 안전하게 역할 검사 (Role 타입 이름은 실제 프로젝트에 맞춰 조정)
111116
if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) {
112117
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다.");
113118
}
114119

115120
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-
}
121+
// 1) 기존 투표가 있는지 조회
122+
var existingOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
123+
if (existingOpt.isPresent()) {
124+
return handleExistingVote(existingOpt.get(), pollOptions, pollId, pollItemsId, memberId);
140125
}
141-
// 기존 투표 내역이 없으면 정상 투표
142-
PollVote pollVote = PollVote.builder()
126+
// 2) 신규 투표 생성
127+
PollVote newVote = PollVote.builder()
143128
.poll(poll)
144129
.pollOptions(pollOptions)
145130
.memberId(memberId)
146131
.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)
132+
PollVote saved = pollVoteRepository.save(newVote);
133+
return buildPollVote(saved, pollId, pollItemsId, memberId, "투표가 완료되었습니다.");
134+
} catch (DataIntegrityViolationException e) {
135+
// 동시성(경합)으로 인해 이미 다른 쓰레드가 투표를 만들어 중복 제약에 걸린 경우 복구 처리
159136
log.warn("중복 투표 시도 감지 - memberId: {}, pollId: {}", memberId, pollId, e);
137+
var existingAfterOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
138+
if (existingAfterOpt.isPresent()) {
139+
return handleExistingVote(existingAfterOpt.get(), pollOptions, pollId, pollItemsId, memberId);
140+
}
160141
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다.");
161142
}
162143
}
163144

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

0 commit comments

Comments
 (0)