diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/repository/VoteRepository.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/repository/VoteRepository.java index b4e2d35f..72cfc2a0 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/repository/VoteRepository.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/repository/VoteRepository.java @@ -1,5 +1,6 @@ package com.umc.yeogi_gal_lae.api.vote.repository; +import com.umc.yeogi_gal_lae.api.tripPlan.domain.TripPlan; import com.umc.yeogi_gal_lae.api.user.domain.User; import com.umc.yeogi_gal_lae.api.vote.domain.Vote; import org.springframework.data.jpa.repository.JpaRepository; @@ -19,11 +20,10 @@ public interface VoteRepository extends JpaRepository { @Query("SELECT v FROM Vote v WHERE v.tripPlan.id = :tripPlanId") List findAllVotesByTripPlanId(@Param("tripPlanId") Long tripPlanId); - // 사용자 ID로 Vote 목록을 조회하는 메서드 추가 - @Query("SELECT u.vote FROM User u WHERE u.id = :userId") - List findByUserId(@Param("userId") Long userId); - @Modifying @Query("DELETE FROM Vote v WHERE v.voteRoom IN (SELECT vr FROM VoteRoom vr WHERE vr.tripPlan.user = :user)") void deleteByVoteRoomUser(@Param("user") User user); + + Optional findByUserAndTripPlan(User user, TripPlan tripPlan); + } diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java index 2442c593..2a4cb658 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java @@ -71,61 +71,52 @@ public VoteResponse.VoteInfoDTO getTripPlanInfoForVote(Long tripId, Long roomId @Transactional - public void createVote(VoteRequest.createVoteReq request, String userEmail){ - - User user = userRepository.findByEmail(userEmail).orElseThrow(()-> new BusinessException(ErrorCode.USER_NOT_FOUND)); - TripPlan tripPlan = tripPlanRepository.findById(request.getTripId()).orElseThrow(()-> new BusinessException(ErrorCode.TRIP_PLAN_NOT_FOUND)); + public void createVote(VoteRequest.createVoteReq request, String userEmail) { + User user = userRepository.findByEmail(userEmail).orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + TripPlan tripPlan = tripPlanRepository.findById(request.getTripId()).orElseThrow(() -> new BusinessException(ErrorCode.TRIP_PLAN_NOT_FOUND)); VoteRoom voteRoom = voteRoomRepository.findByTripPlanId(tripPlan.getId()).orElseThrow(() -> new BusinessException(VOTE_ROOM_NOT_FOUND)); VoteType voteType = VoteType.valueOf(request.getType().trim().toUpperCase()); - // Key 기반의 분산 락을 적용하므로써, 사용자의 동시 투표 방지 + // Key 기반의 분산 락 적용 (동시 투표 방지) RLock lock = redissonClient.getLock("voteLock:" + userEmail); boolean isLocked = false; - try{ + try { isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS); log.info("락 획득 여부: {}, 현재 스레드가 락을 보유하고 있는가? {}", isLocked, lock.isHeldByCurrentThread()); - if (!isLocked) { throw new BusinessException(ErrorCode.VOTE_CONCURRENT_UPDATE);} - - // 락 획득 성공 시, 레디스에 저장된 투표 데이터를 확인하여 캐싱된 데이터가 있을 시, DB 조회 x - Vote vote = getCachedVoteByTripPlan(tripPlan.getId()); - if (vote == null) { - vote = voteRepository.save(Vote.builder() - .tripPlan(tripPlan) - .voteRoom(voteRoom) - .type(voteType) - .build()); - - // 새로 생성된 경우 캐싱 - Vote finalVote = vote; - Optional.ofNullable(cacheManager.getCache("votes")) - .ifPresent(cache -> cache.put(tripPlan.getId(), finalVote)); + if (!isLocked) { + throw new BusinessException(ErrorCode.VOTE_CONCURRENT_UPDATE); } - // 투표 시작 알림 생성 - notificationService.createStartNotification(tripPlan.getRoom().getName(), user.getUsername(), user.getEmail(),NotificationType.VOTE_START, tripPlan.getId(), tripPlan.getTripPlanType()); + // 기존 투표 여부 확인 + Optional existingVote = voteRepository.findByUserAndTripPlan(user, tripPlan); - // 기존 투표 이력 확인 - Vote currentVote = user.getVote(); - if (currentVote == null) { - user.setVote(vote); - } - else if (currentVote.getTripPlan().getId().equals(tripPlan.getId())) { - if (Thread.currentThread().isInterrupted()) { - log.warn("BusinessException 발생 후 스레드가 인터럽트된 상태 - 사용자: {}", userEmail); - Thread.interrupted(); // 인터럽트 상태 초기화 + if (existingVote.isPresent()) { + Vote currentVote = existingVote.get(); + if (currentVote.getType().equals(voteType)) { + throw new BusinessException(ErrorCode.DUPLICATE_VOTE_NOT_ALLOWED); } - if (currentVote.getType().equals(voteType)) { throw new BusinessException(ErrorCode.DUPLICATE_VOTE_NOT_ALLOWED);} - currentVote.setType(voteType); voteRepository.save(currentVote); - - // 사용자가 이미 투표한 내역을 변경할 경우, 레디스에 저장된 데이터를 삭제하여 최신화 - Optional.ofNullable(cacheManager.getCache("votes")) - .ifPresent(cache -> cache.evict(tripPlan.getId())); } else { - user.setVote(vote); + // 새 투표 생성 + Vote newVote = Vote.builder() + .tripPlan(tripPlan) + .voteRoom(voteRoom) + .type(voteType) + .build(); + voteRepository.save(newVote); + + // User 엔티티에 투표 반영 + user.setVote(newVote); + userRepository.save(user); } - userRepository.save(user); + + // 캐시 최신화 (이전 데이터 삭제 후 새로운 데이터 저장) + Optional.ofNullable(cacheManager.getCache("votes")) + .ifPresent(cache -> cache.evict(tripPlan.getId())); + + // 투표 시작 알림 생성 + notificationService.createStartNotification(tripPlan.getRoom().getName(), user.getUsername(), user.getEmail(), NotificationType.VOTE_START, tripPlan.getId(), tripPlan.getTripPlanType()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -133,12 +124,15 @@ else if (currentVote.getTripPlan().getId().equals(tripPlan.getId())) { } catch (Exception e) { log.error("예외 발생 함. 사용자: {} - 예외 타입: {}", userEmail, e.getClass().getName(), e); throw e; - }finally { + } finally { log.info("현재 스레드 사용자 {} 에 대한 락을 해제합니다.", userEmail); - if (isLocked && lock.isHeldByCurrentThread()) { lock.unlock(); } + if (isLocked && lock.isHeldByCurrentThread()) { + lock.unlock(); + } } } + public List getVoteResults(String userEmail, Long tripId){ Long userId = userRepository.findByEmail(userEmail).orElseThrow(()-> new BusinessException(ErrorCode.USER_NOT_FOUND)).getId();