diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/home/converter/HomeConverter.java b/src/main/java/com/umc/yeogi_gal_lae/api/home/converter/HomeConverter.java index 08cf4953..fb978365 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/home/converter/HomeConverter.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/home/converter/HomeConverter.java @@ -15,28 +15,27 @@ public class HomeConverter { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MM-dd"); - public static HomeResponse.OngoingVoteRoom toOngoingVoteRoom(VoteRoom voteRoom) { - - List profileImageUrls = voteRoom.getTripPlan().getRoom().getRoomMembers().stream() - .map(member -> member.getUser().getProfileImage()) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + public static HomeResponse.OngoingVoteRoom toOngoingVoteRoom(TripPlan tripPlan) { + List profileImageUrls = tripPlan.getRoom().getRoomMembers().stream() + .map(member -> member.getUser().getProfileImage()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); return new HomeResponse.OngoingVoteRoom( - voteRoom.getTripPlan().getId(), - voteRoom.getTripPlan().getRoom().getId(), - voteRoom.getTripPlan().getRoom().getMaster().getId(), - voteRoom.getTripPlan().getRoom().getName(), - voteRoom.getTripPlan().getLocation(), - voteRoom.getTripPlan().getRoom().getRoomMembers().size(), - voteRoom.getTripPlan().getVoteLimitTime(), - voteRoom.getTripPlan().getRoom().getRoomMembers().stream().filter(m -> m.getUser().getVote() != null).count(), + tripPlan.getId(), + tripPlan.getRoom().getId(), + tripPlan.getRoom().getMaster().getId(), + tripPlan.getRoom().getName(), + tripPlan.getLocation(), + tripPlan.getRoom().getRoomMembers().size(), + tripPlan.getVoteLimitTime(), + tripPlan.getRoom().getRoomMembers().stream().filter(m -> m.getUser().getVote() != null).count(), profileImageUrls, - voteRoom.getCreatedAt(), - voteRoom.getTripPlan().getTripPlanType(), - voteRoom.getTripPlan().getLatitude(), - voteRoom.getTripPlan().getLongitude() - ); + tripPlan.getCreatedAt(), + tripPlan.getTripPlanType(), + tripPlan.getLatitude(), + tripPlan.getLongitude() + ); } public static HomeResponse.CompletedVoteRoom toCompletedVoteRoom(TripPlan tripPlan, Long aiCourseId) { diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/home/repository/HomeRepository.java b/src/main/java/com/umc/yeogi_gal_lae/api/home/repository/HomeRepository.java index f1aeb7b1..99bb7a26 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/home/repository/HomeRepository.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/home/repository/HomeRepository.java @@ -11,10 +11,9 @@ public interface HomeRepository extends JpaRepository { List findByStatus(Status status); - @Query("SELECT vr FROM VoteRoom vr JOIN vr.tripPlan tp WHERE tp.status = 'ONGOING'") - List findAllOngoingVoteRooms(); + @Query("SELECT tp FROM TripPlan tp WHERE tp.status = 'ONGOING'") + List findAllOngoingTripPlans(); - // 완료된 투표방과 연관된 종료 날짜가 현재 또는 미래인 여행 계획 조회 - @Query("SELECT tp FROM TripPlan tp JOIN tp.voteRoom vr WHERE vr.tripPlan.status = 'COMPLETED' AND tp.endDate >= CURRENT_DATE") - List findCompletedVoteRoomsWithEndDateAfterNow(); + @Query("SELECT tp FROM TripPlan tp WHERE tp.status = 'ONGOING' OR (tp.status = 'COMPLETED' AND tp.tripPlanType = 'COURSE')") + List findAllOngoingAndCompletedCourseTripPlans(); } diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/home/service/HomeService.java b/src/main/java/com/umc/yeogi_gal_lae/api/home/service/HomeService.java index b33e6471..951b34ea 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/home/service/HomeService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/home/service/HomeService.java @@ -7,10 +7,15 @@ import com.umc.yeogi_gal_lae.api.notification.service.NotificationService; import com.umc.yeogi_gal_lae.api.room.repository.RoomMemberRepository; import com.umc.yeogi_gal_lae.api.tripPlan.domain.TripPlan; +import com.umc.yeogi_gal_lae.api.tripPlan.repository.TripPlanRepository; import com.umc.yeogi_gal_lae.api.tripPlan.types.Status; +import com.umc.yeogi_gal_lae.api.tripPlan.types.TripPlanType; import com.umc.yeogi_gal_lae.api.user.domain.User; import com.umc.yeogi_gal_lae.api.user.repository.UserRepository; import com.umc.yeogi_gal_lae.api.vote.domain.VoteRoom; +import com.umc.yeogi_gal_lae.api.vote.dto.request.VoteRoomRequest; +import com.umc.yeogi_gal_lae.api.vote.repository.VoteRoomRepository; +import com.umc.yeogi_gal_lae.api.vote.service.ValidVoteResultService; import com.umc.yeogi_gal_lae.global.error.BusinessException; import com.umc.yeogi_gal_lae.global.error.ErrorCode; import com.umc.yeogi_gal_lae.global.success.SuccessCode; @@ -21,6 +26,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; @Service @@ -28,8 +34,11 @@ public class HomeService { private final HomeRepository homeRepository; + private final TripPlanRepository tripPlanRepository; private final RoomMemberRepository roomMemberRepository; private final UserRepository userRepository; + private final VoteRoomRepository voteRoomRepository; + private final ValidVoteResultService validVoteResultService; private final NotificationService notificationService; private final AICourseRepository aiCourseRepository; @@ -42,9 +51,12 @@ public Response getOngoingVoteRooms(String use .map(roomMember -> roomMember.getRoom().getId()) .collect(Collectors.toList()); - List rooms = homeRepository.findAllOngoingVoteRooms().stream() - .filter(voteRoom -> userRoomIds.contains(voteRoom.getTripPlan().getRoom().getId())) - .filter(voteRoom -> !isVoteTimeExpired(voteRoom)) // ⬅ **제한 시간이 초과된 투표방 제거** + List rooms = homeRepository.findAllOngoingAndCompletedCourseTripPlans().stream() + .filter(tripPlan -> userRoomIds.contains(tripPlan.getRoom().getId())) + .filter(tripPlan -> + (tripPlan.getTripPlanType() == TripPlanType.COURSE) || + (tripPlan.getTripPlanType() == TripPlanType.SCHEDULE && !isVoteTimeExpired(tripPlan.getVoteRoom())) + ) .map(HomeConverter::toOngoingVoteRoom) .collect(Collectors.toList()); @@ -55,24 +67,54 @@ public Response getOngoingVoteRooms(String use // 완료된 투표방과 연관된 종료 날짜가 현재 또는 미래인 여행 계획 조회 public Response getFutureVoteBasedTrips(String userEmail) { User user = userRepository.findByEmail(userEmail) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - // 사용자가 속한 방 ID 목록 조회 List userRoomIds = roomMemberRepository.findAllByUserId(user.getId()) - .stream() - .map(roomMember -> roomMember.getRoom().getId()) - .collect(Collectors.toList()); - - // 완료된 투표방과 연관된 여행 계획 중 종료 날짜가 현재 또는 미래인 여행 필터링 - List rooms = homeRepository.findCompletedVoteRoomsWithEndDateAfterNow().stream() - .filter(tripPlan -> userRoomIds.contains(tripPlan.getRoom().getId())) - .map(tripPlan -> { - Long aiCourseId = aiCourseRepository.findLatestByTripPlanId(tripPlan.getId()) - .map(aiCourse -> aiCourse.getId()) - .orElse(null); - return HomeConverter.toCompletedVoteRoom(tripPlan, aiCourseId); - }) - .collect(Collectors.toList()); + .stream() + .map(roomMember -> roomMember.getRoom().getId()) + .collect(Collectors.toList()); + + List allOngoingTripPlans = homeRepository.findAllOngoingTripPlans(); + + allOngoingTripPlans.forEach(tripPlan -> { + try { + if (tripPlan.getTripPlanType() == TripPlanType.COURSE && isCourseTimeExpired(tripPlan)) { + tripPlan.setStatus(Status.COMPLETED); + tripPlanRepository.save(tripPlan); + } else if (tripPlan.getTripPlanType() == TripPlanType.SCHEDULE) { + VoteRoom voteRoom = tripPlan.getVoteRoom(); + if (voteRoom != null) { + VoteRoomRequest voteRoomRequest = VoteRoomRequest.builder() + .tripId(tripPlan.getId()) + .roomId(tripPlan.getRoom().getId()) + .voteRoomId(voteRoom.getId()) + .build(); + + boolean isVoteFailed = validVoteResultService.validResult(voteRoomRequest); + + if (isVoteFailed) { + voteRoomRepository.delete(voteRoom); + } + } + } + } catch(BusinessException e){ + // VOTE_NOT_COMPLETED_YET 예외 발생 시, 해당 여행을 건너뛰고 나머지 여행들은 계속 조회 + if (!e.getErrorCode().equals(ErrorCode.VOTE_NOT_COMPLETED_YET)) { + throw e; + } + } + }); + + List rooms = homeRepository.findByStatus(Status.COMPLETED).stream() + .filter(tripPlan -> userRoomIds.contains(tripPlan.getRoom().getId())) + .filter(tripPlan -> !tripPlan.getEndDate().isBefore(LocalDate.now())) + .map(tripPlan -> { + Long aiCourseId = aiCourseRepository.findLatestByTripPlanId(tripPlan.getId()) + .map(aiCourse -> aiCourse.getId()) + .orElse(null); + return HomeConverter.toCompletedVoteRoom(tripPlan, aiCourseId); + }) + .collect(Collectors.toList()); return Response.of(SuccessCode.COMPLETED_VOTE_ROOMS_FETCH_OK, new HomeResponse.CompletedVoteRoomList(rooms.size(), rooms)); } @@ -108,6 +150,16 @@ private boolean isVoteTimeExpired(VoteRoom voteRoom) { return LocalDateTime.now().isAfter(voteEndTime); } + private boolean isCourseTimeExpired(TripPlan tripPlan) { + if (tripPlan.getTripPlanType() != TripPlanType.COURSE || tripPlan.getVoteLimitTime() == null) { + return false; + } + + return LocalDateTime.now().isAfter( + tripPlan.getCreatedAt().plusSeconds(tripPlan.getVoteLimitTime().getSeconds()) + ); + } + /** * 특정 사용자의 읽지 않은 알림 여부 반환 (이메일 기반) */ diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/converter/TripPlanConverter.java b/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/converter/TripPlanConverter.java index a8c56a52..e2b551e9 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/converter/TripPlanConverter.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/converter/TripPlanConverter.java @@ -51,7 +51,7 @@ public static TripPlanResponse toResponse(TripPlan tripPlan) { .id(tripPlan.getId()) .roomId(tripPlan.getRoom().getId()) .masterId(tripPlan.getRoom().getMaster().getId()) - .voteRoomId(tripPlan.getVoteRoom().getId()) + .voteRoomId(tripPlan.getVoteRoom() != null ? tripPlan.getVoteRoom().getId() : null) .location(tripPlan.getLocation()) .startDate(tripPlan.getStartDate() != null ? tripPlan.getStartDate().toString() : null) .endDate(tripPlan.getEndDate() != null ? tripPlan.getEndDate().toString() : null) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/service/TripPlanService.java b/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/service/TripPlanService.java index cfc8b10d..4f78febc 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/service/TripPlanService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/tripPlan/service/TripPlanService.java @@ -70,9 +70,16 @@ private void linkTripPlanToRoomMembers(TripPlan tripPlan, Room room) { } /** - * 🚀 여행 계획이 생성되면 자동으로 투표방을 생성하는 메서드 + * 여행 계획이 생성되면 자동으로 투표방을 생성하는 메서드 */ private void createVoteRoomForTrip(TripPlan tripPlan) { + + // 코스 계획(COURSE)일 경우 투표방을 만들지 않고 ONGOING으로 + if (tripPlan.getTripPlanType() == TripPlanType.COURSE) { + tripPlan.setStatus(Status.ONGOING); + return; + } + // 기존에 존재하는 투표방이 있는지 확인 (중복 생성 방지) if (voteRoomRepository.findByTripPlanId(tripPlan.getId()).isPresent()) { throw new BusinessException(ErrorCode.VOTE_ROOM_ALREADY_EXISTS); diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/controller/VoteController.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/controller/VoteController.java index d7ef8c2a..e946a004 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/controller/VoteController.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/controller/VoteController.java @@ -1,5 +1,8 @@ package com.umc.yeogi_gal_lae.api.vote.controller; +import com.umc.yeogi_gal_lae.api.tripPlan.domain.TripPlan; +import com.umc.yeogi_gal_lae.api.tripPlan.repository.TripPlanRepository; +import com.umc.yeogi_gal_lae.api.tripPlan.types.TripPlanType; import com.umc.yeogi_gal_lae.api.vote.AuthenticatedUserUtils; import com.umc.yeogi_gal_lae.api.vote.dto.request.VoteRequest; import com.umc.yeogi_gal_lae.api.vote.dto.VoteResponse; @@ -7,6 +10,7 @@ import com.umc.yeogi_gal_lae.api.vote.service.ValidVoteResultService; import com.umc.yeogi_gal_lae.api.vote.service.VoteService; import com.umc.yeogi_gal_lae.global.common.response.Response; +import com.umc.yeogi_gal_lae.global.error.BusinessException; import com.umc.yeogi_gal_lae.global.error.ErrorCode; import com.umc.yeogi_gal_lae.global.success.SuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +35,7 @@ public class VoteController { @Autowired private final VoteService voteService; private final ValidVoteResultService validVoteResultService; + private final TripPlanRepository tripPlanRepository; @Operation(summary = "투표하고자 하는 여행 계획의 정보 조회 API", description = "현재 투표의 여행 계획 정보에 해당합니다.") @GetMapping("/vote/trip-info") @@ -50,6 +55,13 @@ public Response getTripPlanInfoForVote(@RequestParam @ public Response createVote(@RequestBody @Valid VoteRequest.createVoteReq voteRequest) { String userEmail = AuthenticatedUserUtils.getAuthenticatedUserEmail(); + TripPlan tripPlan = tripPlanRepository.findById(voteRequest.getTripId()) + .orElseThrow(() -> new BusinessException(ErrorCode.TRIP_PLAN_NOT_FOUND)); + + // 코스 계획(COURSE)일 경우 투표 불가능 + if (tripPlan.getTripPlanType() == TripPlanType.COURSE) { + throw new BusinessException(ErrorCode.VOTE_NOT_ALLOWED_FOR_COURSE); + } voteRequest.setUserEmail(userEmail); voteService.createVote(voteRequest, userEmail); diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java b/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java index 737cbb28..337913ab 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java @@ -42,6 +42,7 @@ public enum ErrorCode implements BaseStatus { VOTE_NOT_COMPLETED_YET(HttpStatus.BAD_REQUEST, "VOTE_400", "아직 투표가 종료되지 않았습니다."), VOTE_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "VOTE_401", "요청 하신 투표 방을 찾을 수 없습니다."), VOTE_RESULT_FAILED(HttpStatus.BAD_REQUEST, "VOTE_403", "여행 확정에 실패하셨습니다. 이 방은 사라집니다."), + VOTE_NOT_ALLOWED_FOR_COURSE(HttpStatus.BAD_REQUEST, "VOTE_404", "코스는 투표가 허용되지 않습니다."), // Room Member Error ROOM_MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "ROOM_MEMBER_404", "방에 멤버가 존재하지 않습니다."), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0fda36e0..2471fbc5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -51,7 +51,7 @@ spring: jwt: secret: ${JWT_SECRET} - access-token-validity: 1800000 # 30분 + access-token-validity: 86400000 # 30분 refresh-token-validity: 1209600000 # 2주 access: header: Authorization