Skip to content

Commit 1f4c9b0

Browse files
authored
Merge pull request #361 from prgrms-web-devcourse-final-project/develop
배포
2 parents d650e39 + 9c4568a commit 1f4c9b0

File tree

3 files changed

+67
-59
lines changed

3 files changed

+67
-59
lines changed

.github/workflows/CI-CD_Pipeline.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ jobs:
463463
464464
# prod.env 복원
465465
install -d -m 700 /home/ec2-user/configs
466-
printf "%s" "${{ secrets.PROD_ENV_BASE64 }}" | base64 -d > /home/ec2-user/configs/prod.env
466+
printf "%s" "${{ secrets.PROD_TEST_ENV_BASE64 }}" | base64 -d > /home/ec2-user/configs/prod.env
467467
chmod 600 /home/ec2-user/configs/prod.env
468468
test -s /home/ec2-user/configs/prod.env || { echo "prod.env empty"; exit 1; }
469469

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);

infra/main.tf

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -419,25 +419,30 @@ docker run -d \
419419
-c 'ollama serve & sleep 5 && ollama pull daynice/kure-v1:567m && wait'
420420
421421
echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin
422-
# zookeeper 설치
423-
docker run -d \
424-
--name zookeeper \
425-
--network common \
426-
-p 2181:2181 \
427-
-e ALLOW_ANONYMOUS_LOGIN=yes \
428-
bitnami/zookeeper:latest
429-
430-
# kafka
431-
docker run -d \
432-
--name kafka \
433-
--network common \
434-
-p 9092:9092 \
435-
-e KAFKA_BROKER_ID=1 \
436-
-e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
437-
-e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT \
438-
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 \
439-
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
440-
confluentinc/cp-kafka:7.6.0
422+
# # zookeeper 설치 (카프카용, 필요시 주석 해제)
423+
# docker run -d \
424+
# --name zookeeper \
425+
# --restart unless-stopped \
426+
# --network common \
427+
# -p 2181:2181 \
428+
# -e ZOOKEEPER_CLIENT_PORT=2181 \
429+
# -e ZOOKEEPER_TICK_TIME=2000 \
430+
# confluentinc/cp-zookeeper:7.8.0
431+
#
432+
# # kafka
433+
# docker run -d \
434+
# --name kafka \
435+
# --restart unless-stopped \
436+
# --network common \
437+
# -p 9092:9092 \
438+
# -v kafka-data:/var/lib/kafka/data \
439+
# -e KAFKA_BROKER_ID=1 \
440+
# -e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
441+
# -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT \
442+
# -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
443+
# -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 \
444+
# -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
445+
# confluentinc/cp-kafka:7.8.0
441446
442447
443448

0 commit comments

Comments
 (0)