|
21 | 21 | import java.time.LocalDateTime; |
22 | 22 |
|
23 | 23 | import com.ai.lawyer.global.util.AuthUtil; |
| 24 | +import org.springframework.dao.DataIntegrityViolationException; |
24 | 25 |
|
25 | 26 | @Service |
26 | 27 | @Transactional |
@@ -106,61 +107,64 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) { |
106 | 107 | } |
107 | 108 | PollOptions pollOptions = pollOptionsRepository.findById(pollItemsId) |
108 | 109 | .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 | + |
109 | 114 | Member member = AuthUtil.getMemberOrThrow(memberId); |
110 | | - // USER 또는 ADMIN만 투표 가능 |
| 115 | + // enum 비교로 안전하게 역할 검사 (Role 타입 이름은 실제 프로젝트에 맞춰 조정) |
111 | 116 | if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) { |
112 | 117 | throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다."); |
113 | 118 | } |
114 | 119 |
|
115 | 120 | 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); |
140 | 125 | } |
141 | | - // 기존 투표 내역이 없으면 정상 투표 |
142 | | - PollVote pollVote = PollVote.builder() |
| 126 | + // 2) 신규 투표 생성 |
| 127 | + PollVote newVote = PollVote.builder() |
143 | 128 | .poll(poll) |
144 | 129 | .pollOptions(pollOptions) |
145 | 130 | .memberId(memberId) |
146 | 131 | .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 | + // 동시성(경합)으로 인해 이미 다른 쓰레드가 투표를 만들어 중복 제약에 걸린 경우 복구 처리 |
159 | 136 | 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 | + } |
160 | 141 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다."); |
161 | 142 | } |
162 | 143 | } |
163 | 144 |
|
| 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 | + |
164 | 168 | @Override |
165 | 169 | public PollVoteDto voteByIndex(Long pollId, int index, Long memberId) { |
166 | 170 | List<PollOptions> options = getPollOptions(pollId); |
|
0 commit comments