-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 투표 리워드 시스템 구현 (#175) #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
60d7baa
22646d6
d2c727c
3586ad4
c6d430e
54848de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package hanium.modic.backend.domain.user.entity; | ||
|
|
||
| import org.springframework.data.annotation.Id; | ||
| import org.springframework.data.redis.core.RedisHash; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @RedisHash(value = "userVoteStreak", timeToLive = 60 * 60 * 24 * 7) // 7 days TTL | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class UserVoteStreak { | ||
|
|
||
| @Id | ||
| private Long userId; | ||
|
|
||
| @Builder.Default | ||
| private Integer streakCount = 0; | ||
|
|
||
| public void updateStreak(boolean isCorrect) { | ||
| this.streakCount = isCorrect ? this.streakCount + 1 : 0; | ||
| } | ||
|
|
||
| public void resetStreak() { | ||
| this.streakCount = 0; | ||
| } | ||
|
|
||
| public boolean shouldReceiveReward(int rewardThreshold) { | ||
| return this.streakCount >= rewardThreshold; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package hanium.modic.backend.domain.user.repository; | ||
|
|
||
| import org.springframework.data.repository.CrudRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import hanium.modic.backend.domain.user.entity.UserVoteStreak; | ||
|
|
||
| @Repository | ||
| public interface UserVoteStreakRepository extends CrudRepository<UserVoteStreak, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package hanium.modic.backend.domain.user.service; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hanium.modic.backend.common.error.exception.LockException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hanium.modic.backend.common.property.property.VoteProperties; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hanium.modic.backend.common.redis.distributedLock.LockManager; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hanium.modic.backend.domain.user.entity.UserVoteStreak; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hanium.modic.backend.domain.user.repository.UserVoteStreakRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class UserVoteStreakService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final UserVoteStreakRepository streakRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final LockManager lockManager; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final VoteProperties voteProperties; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public int getStreakCount(Long userId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return streakRepository.findById(userId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(UserVoteStreak::getStreakCount) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .orElse(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void updateStreakWithReset(Long userId, boolean isCorrect) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lockManager.voteStreakLock(userId, () -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UserVoteStreak streak = getOrCreateStreak(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int currentStreak = streak.getStreakCount(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| streak.updateStreak(isCorrect); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 3연속 정답 달성 시 초기화 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isCorrect && streak.getStreakCount() >= voteProperties.getStreakRewardCount()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| streak.resetStreak(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| streakRepository.save(streak); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug("연속 정답 업데이트 (리셋 포함): userId={}, current={}, next={}, isCorrect={}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리셋 시점이 보상 발급과 분리되어 원자성이 깨질 수 있습니다 현재 이 메서드는 임계치 도달 시 streak를 즉시 리셋합니다. 반면 보상 발급은 VoteRewardService에서 잠금 밖에서 이뤄져(동일 사용자 동시 처리 시) 중복 보상 또는 보상 실패 시 진행도 유실이 발생할 수 있습니다. 리셋은 “보상 발급 성공 이후” 동일 락 범위 내에서 실행되도록 책임을 재배치해야 합니다. 다음과 같이 “락 없이 호출하는 내부용 메서드”를 추가해 VoteRewardService가 @@
public void updateStreakWithReset(Long userId, boolean isCorrect) {
try {
lockManager.voteStreakLock(userId, () -> {
UserVoteStreak streak = getOrCreateStreak(userId);
int currentStreak = streak.getStreakCount();
streak.updateStreak(isCorrect);
// 3연속 정답 달성 시 초기화
if (isCorrect && streak.getStreakCount() >= voteProperties.getStreakRewardCount()) {
streak.resetStreak();
}
streakRepository.save(streak);
log.debug("연속 정답 업데이트 (리셋 포함): userId={}, current={}, next={}, isCorrect={}",
userId, currentStreak, streak.getStreakCount(), isCorrect);
});
@@
}
+ /**
+ * 내부용: 외부에서 voteStreakLock을 이미 보유한 상태에서 호출해야 합니다.
+ * 임계구역에서만 사용하세요.
+ */
+ void updateStreakWithResetWithoutLock(Long userId, boolean isCorrect) {
+ UserVoteStreak streak = getOrCreateStreak(userId);
+ int currentStreak = streak.getStreakCount();
+ streak.updateStreak(isCorrect);
+ if (isCorrect && streak.getStreakCount() >= voteProperties.getStreakRewardCount()) {
+ streak.resetStreak();
+ }
+ streakRepository.save(streak);
+ log.debug("[no-lock] 연속 정답 업데이트 (리셋 포함): userId={}, current={}, next={}, isCorrect={}",
+ userId, currentStreak, streak.getStreakCount(), isCorrect);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId, currentStreak, streak.getStreakCount(), isCorrect); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (LockException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("투표 연속 정답 업데이트 (리셋 포함) 락 실패: userId={}", userId, e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new RuntimeException("투표 연속 정답 업데이트에 실패했습니다.", e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private UserVoteStreak getOrCreateStreak(Long userId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return streakRepository.findById(userId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .orElse(UserVoteStreak.builder().userId(userId).build()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainTTL 갱신 보장 여부 확인
다음 스크립트로 엔티티 TTL 설정을 점검할 수 있어요: 🏁 Script executed: #!/bin/bash
rg -nP '@RedisHash|\btimeToLive\b|ttl' -C2 --type=java
rg -nP 'class\s+UserVoteStreak\b' -C3 --type=javaLength of output: 5795 TTL 적용/갱신 보장 필요 — 새 UserVoteStreak은 Redis에 저장(또는 TTL 갱신)되도록 수정하세요
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package hanium.modic.backend.domain.vote.service; | ||
|
|
||
| public record VoteRewardResult(boolean isCorrectAnswer, int currentStreak, boolean receivedTicket) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package hanium.modic.backend.domain.vote.service; | ||
|
|
||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import hanium.modic.backend.common.error.exception.AppException; | ||
| import hanium.modic.backend.common.property.property.VoteProperties; | ||
| import hanium.modic.backend.domain.user.service.UserVoteStreakService; | ||
| import hanium.modic.backend.domain.ticket.service.TicketService; | ||
| import hanium.modic.backend.domain.vote.entity.SimilarityVoteSummaryEntity; | ||
| import hanium.modic.backend.domain.vote.enums.VoteDecision; | ||
| import hanium.modic.backend.domain.vote.repository.SimilarityVoteSummaryRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import static hanium.modic.backend.common.error.ErrorCode.*; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| @Slf4j | ||
| public class VoteRewardService { | ||
|
|
||
| private final SimilarityVoteSummaryRepository voteSummaryRepository; | ||
| private final UserVoteStreakService userVoteStreakService; | ||
| private final VoteProperties voteProperties; | ||
| private final TicketService ticketService; | ||
|
|
||
| public VoteDecision analyzeTrend(Long voteId) { | ||
| SimilarityVoteSummaryEntity summary = voteSummaryRepository.findByVoteId(voteId) | ||
| .orElseThrow(() -> new AppException(VOTE_SUMMARY_NOT_FOUND_EXCEPTION)); | ||
|
|
||
| Long approveWeight = summary.getApproveWeight(); | ||
| Long denyWeight = summary.getDenyWeight(); | ||
|
|
||
| return approveWeight >= denyWeight ? VoteDecision.APPROVE : VoteDecision.DENY; | ||
| } | ||
|
|
||
| public boolean isCorrectAnswer(VoteDecision userDecision, VoteDecision trend) { | ||
| return userDecision == trend; | ||
| } | ||
|
|
||
| @Transactional | ||
| public VoteRewardResult processVoteReward(Long voteId, Long userId, VoteDecision userDecision) { | ||
| try { | ||
| VoteDecision trend = analyzeTrend(voteId); | ||
| boolean isCorrect = isCorrectAnswer(userDecision, trend); | ||
| // streak 업데이트 전 현재 값 확인 | ||
| int currentStreak = userVoteStreakService.getStreakCount(userId); | ||
| boolean willReceiveReward = isCorrect && (currentStreak + 1) >= voteProperties.getStreakRewardCount(); | ||
|
|
||
| // streak 업데이트 (3연속 정답 시 자동 초기화 포함) | ||
| userVoteStreakService.updateStreakWithReset(userId, isCorrect); | ||
|
|
||
| boolean receivedTicket = false; | ||
| if (willReceiveReward) { | ||
| ticketService.giveRewardTicket(userId); | ||
| receivedTicket = true; | ||
| } | ||
|
Comment on lines
+55
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain보상 지급 멱등성 확인
아래 스크립트로 구현을 추적해 보세요: 🏁 Script executed: #!/bin/bash
rg -nP 'class\s+TicketService\b|giveRewardTicket\s*\(' -C3 --type=javaLength of output: 5871 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== rg matches for relevant symbols =="
rg -nP 'class\s+(TicketService|TicketEntity)\b|interface\s+TicketRepository\b|class\s+TicketRepository\b|interface\s+LockManager\b|class\s+LockManager\b|aiRequestTicketLock|giveRewardTicket|getTicketEntity' --hidden --type=java || true
echo
# Print TicketService
TS="src/main/java/hanium/modic/backend/domain/ticket/service/TicketService.java"
if [ -f "$TS" ]; then
echo "----- $TS -----"
sed -n '1,400p' "$TS" || true
echo
fi
# Find and print TicketEntity files
ENT_FILES=$(rg -n --hidden --files-with-matches 'class\s+TicketEntity\b' --type=java || true)
if [ -n "$ENT_FILES" ]; then
for f in $ENT_FILES; do
echo "----- $f -----"
sed -n '1,400p' "$f" || true
echo
done
else
echo "No TicketEntity class file found by rg."
fi
# Find and print TicketRepository files
REP_FILES=$(rg -n --hidden --files-with-matches 'interface\s+TicketRepository\b|class\s+TicketRepository\b' --type=java || true)
if [ -n "$REP_FILES" ]; then
for f in $REP_FILES; do
echo "----- $f -----"
sed -n '1,300p' "$f" || true
echo
done
else
echo "No TicketRepository file found by rg."
fi
# Find and print LockManager files
LOCK_FILES=$(rg -n --hidden --files-with-matches 'interface\s+LockManager\b|class\s+LockManager\b|aiRequestTicketLock' --type=java || true)
if [ -n "$LOCK_FILES" ]; then
for f in $LOCK_FILES; do
echo "----- $f -----"
sed -n '1,300p' "$f" || true
echo
done
else
echo "No LockManager file found by rg."
fi
# Search for repository methods related to userId / unique constraints on Ticket
echo "== search for 'userId' or '@Table' or 'unique' in ticket package =="
rg -n --hidden 'userId|@Table|uniqueConstraint|uniqueConstraints|@Column\(|unique' src/main/java/hanium/modic/backend/domain/ticket -S || trueLength of output: 16211 멱등성 미보장 — giveRewardTicket 중복 호출 시 티켓이 누적 지급됩니다 aiRequestTicketLock로 동시성은 직렬화되고 TicketEntity.user_id에 DB 유니크 제약이 있지만, giveRewardTicket는 ticketCount를 단순 증가시키므로 재시도/중복 이벤트에 대해 단일 지급을 보장하지 않습니다.
🤖 Prompt for AI Agents |
||
|
|
||
| // 업데이트 후 streak 값 (리워드 받았으면 0, 아니면 업데이트된 값) | ||
| int finalStreak = willReceiveReward ? 0 : userVoteStreakService.getStreakCount(userId); | ||
| log.info("투표 리워드 처리 완료: voteId={}, userId={}, userDecision={}, trend={}, isCorrect={}", | ||
| voteId, userId, userDecision, trend, isCorrect); | ||
| return new VoteRewardResult(isCorrect, finalStreak, receivedTicket); | ||
| } catch (Exception e) { | ||
| log.error("투표 리워드 처리 중 오류 발생: voteId={}, userId={}", voteId, userId, e); | ||
| return new VoteRewardResult(false, userVoteStreakService.getStreakCount(userId), false); | ||
| } | ||
|
Comment on lines
+43
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 치명적 TOCTOU: 리워드 결정·리셋·티켓 지급이 동일 락에서 원자적으로 수행되지 않음 현재 흐름:
결과: 같은 사용자의 동시 요청에서 중복 지급/미지급, 보상 실패 시 진행도 유실(리셋 후 예외) 가능. 보상 발급과 리셋을 동일 다음 패치를 제안합니다(동일 키로 잠금, 보상→리셋 순, 리턴 값 계산 일관화). 전제: @@
-import hanium.modic.backend.common.property.property.VoteProperties;
+import hanium.modic.backend.common.property.property.VoteProperties;
+import hanium.modic.backend.common.error.exception.LockException;
+import hanium.modic.backend.common.redis.distributedLock.LockManager;
+import java.util.concurrent.atomic.AtomicReference;
@@
- private final VoteProperties voteProperties;
+ private final VoteProperties voteProperties;
private final TicketService ticketService;
+ private final LockManager lockManager;
@@
- @Transactional
- public VoteRewardResult processVoteReward(Long voteId, Long userId, VoteDecision userDecision) {
- try {
- VoteDecision trend = analyzeTrend(voteId);
- boolean isCorrect = isCorrectAnswer(userDecision, trend);
- // streak 업데이트 전 현재 값 확인
- int currentStreak = userVoteStreakService.getStreakCount(userId);
- boolean willReceiveReward = isCorrect && (currentStreak + 1) >= voteProperties.getStreakRewardCount();
-
- // streak 업데이트 (3연속 정답 시 자동 초기화 포함)
- userVoteStreakService.updateStreakWithReset(userId, isCorrect);
-
- boolean receivedTicket = false;
- if (willReceiveReward) {
- ticketService.giveRewardTicket(userId);
- receivedTicket = true;
- }
-
- // 업데이트 후 streak 값 (리워드 받았으면 0, 아니면 업데이트된 값)
- int finalStreak = willReceiveReward ? 0 : userVoteStreakService.getStreakCount(userId);
- log.info("투표 리워드 처리 완료: voteId={}, userId={}, userDecision={}, trend={}, isCorrect={}",
- voteId, userId, userDecision, trend, isCorrect);
- return new VoteRewardResult(isCorrect, finalStreak, receivedTicket);
- } catch (Exception e) {
- log.error("투표 리워드 처리 중 오류 발생: voteId={}, userId={}", voteId, userId, e);
- return new VoteRewardResult(false, userVoteStreakService.getStreakCount(userId), false);
- }
- }
+ @Transactional
+ public VoteRewardResult processVoteReward(Long voteId, Long userId, VoteDecision userDecision) {
+ try {
+ AtomicReference<VoteRewardResult> ref = new AtomicReference<>();
+ lockManager.voteStreakLock(userId, () -> {
+ VoteDecision trend = analyzeTrend(voteId);
+ boolean isCorrect = isCorrectAnswer(userDecision, trend);
+ int current = userVoteStreakService.getStreakCount(userId);
+ boolean received = false;
+ int finalStreak;
+
+ if (isCorrect) {
+ if (current + 1 >= voteProperties.getStreakRewardCount()) {
+ // 보상 성공 후 리셋
+ ticketService.giveRewardTicket(userId);
+ received = true;
+ userVoteStreakService.updateStreakWithResetWithoutLock(userId, true);
+ finalStreak = 0;
+ } else {
+ userVoteStreakService.updateStreakWithResetWithoutLock(userId, true);
+ finalStreak = current + 1;
+ }
+ } else {
+ userVoteStreakService.updateStreakWithResetWithoutLock(userId, false);
+ finalStreak = 0;
+ }
+
+ log.info("투표 리워드 처리 완료: voteId={}, userId={}, userDecision={}, trend={}, isCorrect={}, finalStreak={}, receivedTicket={}",
+ voteId, userId, userDecision, trend, isCorrect, finalStreak, received);
+ ref.set(new VoteRewardResult(isCorrect, finalStreak, received));
+ });
+ return ref.get();
+ } catch (LockException e) {
+ log.error("투표 리워드 락 실패: voteId={}, userId={}", voteId, userId, e);
+ return new VoteRewardResult(false, userVoteStreakService.getStreakCount(userId), false);
+ } catch (Exception e) {
+ log.error("투표 리워드 처리 중 오류 발생: voteId={}, userId={}", voteId, userId, e);
+ return new VoteRewardResult(false, userVoteStreakService.getStreakCount(userId), false);
+ }
+ }보완 권장:
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| package hanium.modic.backend.web.vote.dto.response; | ||
|
|
||
| public record VoteParticipationResponse(Long voteId) { | ||
|
|
||
| public static VoteParticipationResponse of(Long voteId) { | ||
| return new VoteParticipationResponse(voteId); | ||
| public record VoteParticipationResponse(Long voteId, boolean isCorrectAnswer, int currentStreak, | ||
| boolean receivedTicket) { | ||
|
|
||
| public static VoteParticipationResponse of(Long voteId, boolean isCorrectAnswer, int currentStreak, | ||
| boolean receivedTicket) { | ||
| return new VoteParticipationResponse(voteId, isCorrectAnswer, currentStreak, receivedTicket); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
보상 티켓 누적 시 오버플로와 일일 초기화 정책 충돌 가능성
적용 예시(오버플로 방어):
reset 정책(보상 보전) 변경 예시 — 변경은 본 범위 밖이므로 참조만 드립니다:
정책이 “보상 포함 전량 리셋”이라면 문서화와 테스트 케이스로 의도를 명확히 해 주세요.
🏁 Script executed:
Length of output: 233
🏁 Script executed:
Length of output: 5078
오버플로우 방지 + 일일 리셋으로 인한 보상 티켓 소실 방지 — 수정 필요
🤖 Prompt for AI Agents