diff --git a/src/main/java/team/washer/server/v2/domain/admin/service/impl/QueryAdminDashboardServiceImpl.java b/src/main/java/team/washer/server/v2/domain/admin/service/impl/QueryAdminDashboardServiceImpl.java index 4c88c0ea..3be5d79f 100644 --- a/src/main/java/team/washer/server/v2/domain/admin/service/impl/QueryAdminDashboardServiceImpl.java +++ b/src/main/java/team/washer/server/v2/domain/admin/service/impl/QueryAdminDashboardServiceImpl.java @@ -32,7 +32,7 @@ public class QueryAdminDashboardServiceImpl implements QueryAdminDashboardServic public AdminDashboardResDto execute() { log.info("Querying admin dashboard statistics"); - var activeReservations = reservationRepository.countActiveReservations(); + var activeReservations = reservationRepository.countCurrentlyActive(); var pendingReports = malfunctionReportRepository.countByStatus(MalfunctionReportStatus.PENDING); var processingReports = malfunctionReportRepository.countByStatus(MalfunctionReportStatus.IN_PROGRESS); var completedReports = malfunctionReportRepository.countByStatus(MalfunctionReportStatus.RESOLVED); diff --git a/src/main/java/team/washer/server/v2/domain/machine/service/impl/DeleteMachineServiceImpl.java b/src/main/java/team/washer/server/v2/domain/machine/service/impl/DeleteMachineServiceImpl.java index 87857acf..8eb002ec 100644 --- a/src/main/java/team/washer/server/v2/domain/machine/service/impl/DeleteMachineServiceImpl.java +++ b/src/main/java/team/washer/server/v2/domain/machine/service/impl/DeleteMachineServiceImpl.java @@ -24,7 +24,7 @@ public DeleteMachineResDto execute(Long machineId) { final var machine = machineRepository.findById(machineId) .orElseThrow(() -> new ExpectedException("기기를 찾을 수 없습니다", HttpStatus.NOT_FOUND)); - if (reservationRepository.findActiveReservationByMachineId(machineId).isPresent()) { + if (reservationRepository.findCurrentlyActiveReservationByMachineId(machineId).isPresent()) { throw new ExpectedException("활성 예약이 존재하는 기기는 삭제할 수 없습니다", HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/team/washer/server/v2/domain/machine/service/impl/QueryAllMachinesStatusServiceImpl.java b/src/main/java/team/washer/server/v2/domain/machine/service/impl/QueryAllMachinesStatusServiceImpl.java index a389474d..2542c88d 100644 --- a/src/main/java/team/washer/server/v2/domain/machine/service/impl/QueryAllMachinesStatusServiceImpl.java +++ b/src/main/java/team/washer/server/v2/domain/machine/service/impl/QueryAllMachinesStatusServiceImpl.java @@ -55,7 +55,8 @@ public List execute(Long userId, boolean sorted) { var deviceStatusMap = deviceStatusQuerySupport.queryAllDevicesStatus(deviceIds); var results = machines.stream().map(machine -> { - var reservation = reservationRepository.findActiveReservationByMachineId(machine.getId()).orElse(null); + var reservation = reservationRepository.findCurrentlyActiveReservationByMachineId(machine.getId()) + .orElse(null); return mapToStatusDto(machine, deviceStatusMap.get(machine.getDeviceId()), reservation); }).toList(); diff --git a/src/main/java/team/washer/server/v2/domain/machine/service/impl/UpdateMachineStatusServiceImpl.java b/src/main/java/team/washer/server/v2/domain/machine/service/impl/UpdateMachineStatusServiceImpl.java index 61e8f413..70a2c2b1 100644 --- a/src/main/java/team/washer/server/v2/domain/machine/service/impl/UpdateMachineStatusServiceImpl.java +++ b/src/main/java/team/washer/server/v2/domain/machine/service/impl/UpdateMachineStatusServiceImpl.java @@ -34,8 +34,9 @@ public MachineStatusUpdateResDto execute(Long machineId, MachineStatus status) { } else if (status == MachineStatus.NORMAL) { machine.markAsNormal(); - // 복구 후 실제 예약 상태에 맞게 availability 재동기화 - findActiveReservation(machine).ifPresent(reservation -> { + // 복구 후 실제 예약 상태에 맞게 availability 재동기화. 만료된 RESERVED 예약은 활성으로 보지 않으므로 + // markAsNormal()이 설정한 AVAILABLE이 그대로 유지된다 + findCurrentlyActiveReservation(machine).ifPresent(reservation -> { switch (reservation.getStatus()) { case RESERVED -> machine.markAsReserved(); case RUNNING -> machine.markAsInUse(); @@ -53,7 +54,7 @@ public MachineStatusUpdateResDto execute(Long machineId, MachineStatus status) { savedMachine.getAvailability()); } - private Optional findActiveReservation(Machine machine) { - return reservationRepository.findActiveReservationByMachineId(machine.getId()); + private Optional findCurrentlyActiveReservation(Machine machine) { + return reservationRepository.findCurrentlyActiveReservationByMachineId(machine.getId()); } } diff --git a/src/main/java/team/washer/server/v2/domain/reservation/repository/ReservationRepository.java b/src/main/java/team/washer/server/v2/domain/reservation/repository/ReservationRepository.java index b7752023..4d65c8bc 100644 --- a/src/main/java/team/washer/server/v2/domain/reservation/repository/ReservationRepository.java +++ b/src/main/java/team/washer/server/v2/domain/reservation/repository/ReservationRepository.java @@ -44,23 +44,21 @@ default List findAllActiveReservations() { return findByStatusIn(List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING)); } - @Query("SELECT COUNT(r) FROM Reservation r WHERE r.status IN :statuses") - long countAllActiveReservations(@Param("statuses") List statuses); - - default long countActiveReservations() { - return countAllActiveReservations(List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING)); - } - @Query("SELECT r FROM Reservation r WHERE r.machine.id = :machineId AND r.status IN :statuses ORDER BY r.createdAt DESC") List findFirstActiveReservationByMachineId(@Param("machineId") Long machineId, @Param("statuses") List statuses); /** - * 기기의 대표 활성 예약을 조회한다. 선택 규칙은 {@link ActiveReservationSelector}가 정의한다. + * 기기의 대표 활성 예약을 조회한다. 만료 예약 제외는 + * {@link ReservationRepositoryCustom#findCurrentlyActiveByMachineId(Long)}가 쿼리 + * 단계에서 처리하고, 남은 후보 중 대표를 고르는 규칙은 {@link ActiveReservationSelector}가 정의한다. + * + *

+ * 타임아웃이 지난 RESERVED 예약만 남은 기기는 {@link Optional#empty()}가 된다. 스케줄러가 아직 정리하지 못한 + * 만료 예약이 기기를 점유한 것처럼 보이게 하지 않기 위함이다. */ - default Optional findActiveReservationByMachineId(Long machineId) { - return ActiveReservationSelector.selectPrimary(findFirstActiveReservationByMachineId(machineId, - List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING))); + default Optional findCurrentlyActiveReservationByMachineId(Long machineId) { + return ActiveReservationSelector.selectPrimary(findCurrentlyActiveByMachineId(machineId)); } @Query("SELECT COUNT(r) FROM Reservation r WHERE r.machine = :machine AND r.status IN :statuses") @@ -76,6 +74,4 @@ default List findAllRunningReservations() { @Query("SELECT DISTINCT r.machine.id FROM Reservation r WHERE r.status IN :statuses") List findMachineIdsByStatusIn(@Param("statuses") List statuses); - - boolean existsByUserAndStatusIn(User user, List statuses); } diff --git a/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/ReservationRepositoryCustom.java b/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/ReservationRepositoryCustom.java index c9265c7c..b373897a 100644 --- a/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/ReservationRepositoryCustom.java +++ b/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/ReservationRepositoryCustom.java @@ -84,6 +84,38 @@ Page findAllWithFilters(String userName, */ List findCurrentlyActiveByRoomNumber(String roomNumber); + /** + * 기기 ID로 현재 활성 예약 목록을 조회합니다. 타임아웃이 지난 RESERVED 예약은 쿼리 단계에서 제외됩니다. + * + * @param machineId + * 조회 대상 기기 ID + * @return 만료되지 않은 활성 예약 목록 (createdAt 내림차순) + */ + List findCurrentlyActiveByMachineId(Long machineId); + + /** + * 현재 활성 예약이 걸려 있는 기기 ID 목록을 조회합니다. 타임아웃이 지난 RESERVED 예약만 남은 기기는 포함되지 않습니다. + * + * @return 만료되지 않은 활성 예약이 있는 기기 ID 목록 + */ + List findCurrentlyActiveMachineIds(); + + /** + * 현재 활성 예약 수를 반환합니다. 타임아웃이 지난 RESERVED 예약은 집계에서 제외됩니다. + * + * @return 만료되지 않은 활성 예약 수 + */ + long countCurrentlyActive(); + + /** + * 사용자에게 현재 활성 예약이 있는지 반환합니다. 타임아웃이 지난 RESERVED 예약만 남아 있으면 거짓입니다. + * + * @param user + * 조회 대상 사용자 + * @return 만료되지 않은 활성 예약 존재 여부 + */ + boolean existsCurrentlyActiveByUser(User user); + /** * 기기별 예약 히스토리 조회 * diff --git a/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/impl/ReservationRepositoryCustomImpl.java b/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/impl/ReservationRepositoryCustomImpl.java index b44bb8b5..44abcb53 100644 --- a/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/impl/ReservationRepositoryCustomImpl.java +++ b/src/main/java/team/washer/server/v2/domain/reservation/repository/custom/impl/ReservationRepositoryCustomImpl.java @@ -111,6 +111,33 @@ public List findCurrentlyActiveByRoomNumber(String roomNumber) { .orderBy(reservation.createdAt.desc()).fetch(); } + @Override + public List findCurrentlyActiveByMachineId(Long machineId) { + return jpaQueryFactory.selectFrom(reservation).join(reservation.machine, machine).fetchJoin() + .join(reservation.user, user).fetchJoin().where(reservation.machine.id.eq(machineId), currentlyActive()) + .orderBy(reservation.createdAt.desc()).fetch(); + } + + @Override + public List findCurrentlyActiveMachineIds() { + return jpaQueryFactory.select(reservation.machine.id).distinct().from(reservation).where(currentlyActive()) + .fetch(); + } + + @Override + public long countCurrentlyActive() { + final var total = jpaQueryFactory.select(reservation.count()).from(reservation).where(currentlyActive()) + .fetchOne(); + + return total != null ? total : 0L; + } + + @Override + public boolean existsCurrentlyActiveByUser(User targetUser) { + return jpaQueryFactory.selectOne().from(reservation).where(reservation.user.eq(targetUser), currentlyActive()) + .fetchFirst() != null; + } + /** * 만료되지 않은 활성 예약 조건을 반환합니다. {@link Reservation#isCurrentlyActive()}와 동일한 규칙을 쿼리 * 조건으로 표현한 것으로, 전체를 로드한 뒤 메모리에서 거르지 않도록 합니다. diff --git a/src/main/java/team/washer/server/v2/domain/smartthings/service/impl/ShutdownIdleMachinesServiceImpl.java b/src/main/java/team/washer/server/v2/domain/smartthings/service/impl/ShutdownIdleMachinesServiceImpl.java index 1a2ecbec..71f30367 100644 --- a/src/main/java/team/washer/server/v2/domain/smartthings/service/impl/ShutdownIdleMachinesServiceImpl.java +++ b/src/main/java/team/washer/server/v2/domain/smartthings/service/impl/ShutdownIdleMachinesServiceImpl.java @@ -1,7 +1,6 @@ package team.washer.server.v2.domain.smartthings.service.impl; import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Set; @@ -12,7 +11,6 @@ import lombok.extern.slf4j.Slf4j; import team.washer.server.v2.domain.machine.entity.Machine; import team.washer.server.v2.domain.machine.repository.MachineRepository; -import team.washer.server.v2.domain.reservation.enums.ReservationStatus; import team.washer.server.v2.domain.reservation.repository.ReservationRepository; import team.washer.server.v2.domain.smartthings.exception.SmartThingsPermissionException; import team.washer.server.v2.domain.smartthings.service.ShutdownIdleMachinesService; @@ -33,9 +31,6 @@ public class ShutdownIdleMachinesServiceImpl implements ShutdownIdleMachinesServ @Autowired(required = false) private DiscordErrorNotificationService discordErrorNotificationService; - private static final List ACTIVE_STATUSES = List.of(ReservationStatus.RESERVED, - ReservationStatus.RUNNING); - @Override public void execute() { var machines = machineRepository.findAll(); @@ -43,7 +38,8 @@ public void execute() { return; } - var activeMachineIds = Set.copyOf(reservationRepository.findMachineIdsByStatusIn(ACTIVE_STATUSES)); + // 만료 예약만 남은 기기는 실제로 아무도 쓰지 않으므로 유휴 전원 차단 대상에 포함한다 + var activeMachineIds = Set.copyOf(reservationRepository.findCurrentlyActiveMachineIds()); var idleCandidates = machines.stream().filter(machine -> !activeMachineIds.contains(machine.getId())).toList(); var skippedActiveCount = machines.size() - idleCandidates.size(); diff --git a/src/main/java/team/washer/server/v2/domain/user/service/impl/DeleteUserServiceImpl.java b/src/main/java/team/washer/server/v2/domain/user/service/impl/DeleteUserServiceImpl.java index 0ced157e..6c4d3a8e 100644 --- a/src/main/java/team/washer/server/v2/domain/user/service/impl/DeleteUserServiceImpl.java +++ b/src/main/java/team/washer/server/v2/domain/user/service/impl/DeleteUserServiceImpl.java @@ -1,14 +1,11 @@ package team.washer.server.v2.domain.user.service.impl; -import java.util.List; - import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import team.themoment.sdk.exception.ExpectedException; -import team.washer.server.v2.domain.reservation.enums.ReservationStatus; import team.washer.server.v2.domain.reservation.repository.ReservationRepository; import team.washer.server.v2.domain.user.repository.UserRepository; import team.washer.server.v2.domain.user.service.DeleteUserService; @@ -29,9 +26,8 @@ public void execute(Long userId) { final var user = userRepository.findById(userId) .orElseThrow(() -> new ExpectedException("사용자를 찾을 수 없습니다", HttpStatus.NOT_FOUND)); - // 활성 예약이 있는지 확인 - final var activeStatuses = List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING); - final boolean hasActiveReservations = reservationRepository.existsByUserAndStatusIn(user, activeStatuses); + // 만료되지 않은 활성 예약이 있는지 확인. 만료 예약만 남은 사용자는 본인 탈퇴와 동일하게 삭제할 수 있어야 한다 + final boolean hasActiveReservations = reservationRepository.existsCurrentlyActiveByUser(user); if (hasActiveReservations) { throw new ExpectedException("활성 예약이 있는 사용자는 삭제할 수 없습니다", HttpStatus.BAD_REQUEST); diff --git a/src/test/java/team/washer/server/v2/domain/admin/service/QueryAdminDashboardServiceTest.java b/src/test/java/team/washer/server/v2/domain/admin/service/QueryAdminDashboardServiceTest.java index da512a6d..9ca9602a 100644 --- a/src/test/java/team/washer/server/v2/domain/admin/service/QueryAdminDashboardServiceTest.java +++ b/src/test/java/team/washer/server/v2/domain/admin/service/QueryAdminDashboardServiceTest.java @@ -46,7 +46,7 @@ class ExecuteTest { @DisplayName("활성 예약, 고장 신고, 기기, 세탁정지 학생 통계를 성공적으로 조회한다") void execute_ShouldReturnDashboardStatistics_WhenDataExists() { // Given - when(reservationRepository.countActiveReservations()).thenReturn(5L); + when(reservationRepository.countCurrentlyActive()).thenReturn(5L); when(malfunctionReportRepository.countByStatus(MalfunctionReportStatus.PENDING)).thenReturn(3L); when(malfunctionReportRepository.countByStatus(MalfunctionReportStatus.IN_PROGRESS)).thenReturn(2L); when(malfunctionReportRepository.countByStatus(MalfunctionReportStatus.RESOLVED)).thenReturn(10L); @@ -72,7 +72,7 @@ void execute_ShouldReturnDashboardStatistics_WhenDataExists() { @DisplayName("데이터가 없으면 모든 통계가 0으로 반환된다") void execute_ShouldReturnZeroStatistics_WhenNoDataExists() { // Given - when(reservationRepository.countActiveReservations()).thenReturn(0L); + when(reservationRepository.countCurrentlyActive()).thenReturn(0L); when(malfunctionReportRepository.countByStatus(MalfunctionReportStatus.PENDING)).thenReturn(0L); when(malfunctionReportRepository.countByStatus(MalfunctionReportStatus.IN_PROGRESS)).thenReturn(0L); when(malfunctionReportRepository.countByStatus(MalfunctionReportStatus.RESOLVED)).thenReturn(0L); diff --git a/src/test/java/team/washer/server/v2/domain/machine/service/DeleteMachineServiceTest.java b/src/test/java/team/washer/server/v2/domain/machine/service/DeleteMachineServiceTest.java index 22ebb59e..489ecddf 100644 --- a/src/test/java/team/washer/server/v2/domain/machine/service/DeleteMachineServiceTest.java +++ b/src/test/java/team/washer/server/v2/domain/machine/service/DeleteMachineServiceTest.java @@ -62,7 +62,8 @@ void it_deletes_machine_and_returns_info() { var machineId = 1L; var machine = createMachine(); given(machineRepository.findById(machineId)).willReturn(Optional.of(machine)); - given(reservationRepository.findActiveReservationByMachineId(machineId)).willReturn(Optional.empty()); + given(reservationRepository.findCurrentlyActiveReservationByMachineId(machineId)) + .willReturn(Optional.empty()); // When var result = deleteMachineService.execute(machineId); @@ -85,7 +86,7 @@ void it_throws_bad_request_exception() { var machineId = 1L; var machine = createMachine(); given(machineRepository.findById(machineId)).willReturn(Optional.of(machine)); - given(reservationRepository.findActiveReservationByMachineId(machineId)) + given(reservationRepository.findCurrentlyActiveReservationByMachineId(machineId)) .willReturn(Optional.of(activeReservation)); // When & Then diff --git a/src/test/java/team/washer/server/v2/domain/machine/service/QueryAllMachinesStatusServiceTest.java b/src/test/java/team/washer/server/v2/domain/machine/service/QueryAllMachinesStatusServiceTest.java index 5e305105..4f9f8514 100644 --- a/src/test/java/team/washer/server/v2/domain/machine/service/QueryAllMachinesStatusServiceTest.java +++ b/src/test/java/team/washer/server/v2/domain/machine/service/QueryAllMachinesStatusServiceTest.java @@ -98,7 +98,7 @@ void execute_ShouldReturnSortedMachinesStatus_WhenSortedIsTrue() { when(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1", "device-2"))) .thenReturn(Map.of("device-1", deviceStatus, "device-2", deviceStatus)); - when(reservationRepository.findActiveReservationByMachineId(any())).thenReturn(Optional.empty()); + when(reservationRepository.findCurrentlyActiveReservationByMachineId(any())).thenReturn(Optional.empty()); // When var result = queryAllMachinesStatusService.execute(USER_ID, true); @@ -122,7 +122,7 @@ void execute_ShouldReturnUnsortedMachinesStatus_WhenSortedIsFalse() { when(machineRepository.findAll()).thenReturn(List.of(machine1)); when(deviceStatusQuerySupport.queryAllDevicesStatus(any())).thenReturn(Map.of()); - when(reservationRepository.findActiveReservationByMachineId(any())).thenReturn(Optional.empty()); + when(reservationRepository.findCurrentlyActiveReservationByMachineId(any())).thenReturn(Optional.empty()); // When var result = queryAllMachinesStatusService.execute(USER_ID, false); @@ -174,7 +174,8 @@ void execute_ShouldUseDryerCompletionTime_WhenDryerStatusContainsWasherAndDryerC when(machineRepository.findAll(any(Sort.class))).thenReturn(List.of(machine)); when(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1"))) .thenReturn(Map.of("device-1", deviceStatus)); - when(reservationRepository.findActiveReservationByMachineId(any())).thenReturn(Optional.of(reservation)); + when(reservationRepository.findCurrentlyActiveReservationByMachineId(any())) + .thenReturn(Optional.of(reservation)); when(reservation.getStatus()).thenReturn(ReservationStatus.RUNNING); when(reservation.getUser()).thenReturn(user); @@ -211,7 +212,8 @@ void execute_ShouldKeepReservationInfo_WhenDeviceReportsCompletionButReservation when(machineRepository.findAll(any(Sort.class))).thenReturn(List.of(machine)); when(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1"))) .thenReturn(Map.of("device-1", deviceStatus)); - when(reservationRepository.findActiveReservationByMachineId(any())).thenReturn(Optional.of(reservation)); + when(reservationRepository.findCurrentlyActiveReservationByMachineId(any())) + .thenReturn(Optional.of(reservation)); when(reservation.getId()).thenReturn(10L); when(reservation.getStatus()).thenReturn(ReservationStatus.RUNNING); when(reservation.getUser()).thenReturn(user); @@ -272,7 +274,7 @@ private void givenMachineWithReservation(Machine machine, Reservation reservatio givenUserMocked(); when(machineRepository.findAll(any(Sort.class))).thenReturn(List.of(machine)); when(deviceStatusQuerySupport.queryAllDevicesStatus(any())).thenReturn(Map.of()); - when(reservationRepository.findActiveReservationByMachineId(any())) + when(reservationRepository.findCurrentlyActiveReservationByMachineId(any())) .thenReturn(Optional.ofNullable(reservationOrNull)); } diff --git a/src/test/java/team/washer/server/v2/domain/machine/service/UpdateMachineStatusServiceTest.java b/src/test/java/team/washer/server/v2/domain/machine/service/UpdateMachineStatusServiceTest.java index 9de919c5..25d5ba4e 100644 --- a/src/test/java/team/washer/server/v2/domain/machine/service/UpdateMachineStatusServiceTest.java +++ b/src/test/java/team/washer/server/v2/domain/machine/service/UpdateMachineStatusServiceTest.java @@ -102,7 +102,7 @@ void it_changes_machine_to_normal_and_available() { given(machine.getId()).willReturn(machineId); given(machineRepository.findById(machineId)).willReturn(Optional.of(machine)); - given(reservationRepository.findActiveReservationByMachineId(machineId)) + given(reservationRepository.findCurrentlyActiveReservationByMachineId(machineId)) .willReturn(Optional.empty()); given(machineRepository.save(any(Machine.class))) .willAnswer(invocation -> invocation.getArgument(0)); @@ -115,7 +115,7 @@ void it_changes_machine_to_normal_and_available() { assertThat(result.status()).isEqualTo(MachineStatus.NORMAL); assertThat(result.availability()).isEqualTo(MachineAvailability.AVAILABLE); then(machineRepository).should(times(1)).findById(machineId); - then(reservationRepository).should(times(1)).findActiveReservationByMachineId(machineId); + then(reservationRepository).should(times(1)).findCurrentlyActiveReservationByMachineId(machineId); then(machineRepository).should(times(1)).save(any(Machine.class)); } } @@ -134,7 +134,7 @@ void it_changes_machine_to_normal_and_reserved() { Reservation reservation = createReservation(machine, ReservationStatus.RESERVED); given(machineRepository.findById(machineId)).willReturn(Optional.of(machine)); - given(reservationRepository.findActiveReservationByMachineId(machineId)) + given(reservationRepository.findCurrentlyActiveReservationByMachineId(machineId)) .willReturn(Optional.of(reservation)); given(machineRepository.save(any(Machine.class))) .willAnswer(invocation -> invocation.getArgument(0)); @@ -147,7 +147,7 @@ void it_changes_machine_to_normal_and_reserved() { assertThat(result.status()).isEqualTo(MachineStatus.NORMAL); assertThat(result.availability()).isEqualTo(MachineAvailability.RESERVED); then(machineRepository).should(times(1)).findById(machineId); - then(reservationRepository).should(times(1)).findActiveReservationByMachineId(machineId); + then(reservationRepository).should(times(1)).findCurrentlyActiveReservationByMachineId(machineId); then(machineRepository).should(times(1)).save(any(Machine.class)); } } diff --git a/src/test/java/team/washer/server/v2/domain/reservation/repository/ReservationRepositoryCurrentlyActiveTest.java b/src/test/java/team/washer/server/v2/domain/reservation/repository/ReservationRepositoryCurrentlyActiveTest.java index d649ed91..cf4ab200 100644 --- a/src/test/java/team/washer/server/v2/domain/reservation/repository/ReservationRepositoryCurrentlyActiveTest.java +++ b/src/test/java/team/washer/server/v2/domain/reservation/repository/ReservationRepositoryCurrentlyActiveTest.java @@ -50,7 +50,9 @@ class ReservationRepositoryCurrentlyActiveTest { private TestEntityManager entityManager; private User owner; + private User expiredOnlyOwner; private Machine washer; + private Machine expiredOnlyMachine; private Reservation freshReserved; private Reservation justBeforeTimeoutReserved; @@ -83,6 +85,14 @@ void setUp() { completed = persistReservation(ReservationStatus.COMPLETED, now.minusMinutes(1)); cancelled = persistReservation(ReservationStatus.CANCELLED, now.minusMinutes(1)); + // 만료된 RESERVED 예약만 남은 사용자와 기기. 스케줄러가 아직 정리하지 못한 상태를 재현한다 + expiredOnlyOwner = persist( + User.builder().name("이건조").studentId("2102").roomNumber("302").grade(1).floor(3).build()); + expiredOnlyMachine = persist(Machine.builder().name("W3R1").type(MachineType.WASHER).deviceId("device-w3r1") + .floor(3).position(Position.RIGHT).number(1).build()); + persist(Reservation.builder().user(expiredOnlyOwner).machine(expiredOnlyMachine) + .status(ReservationStatus.RESERVED).reservedAt(now.minusMinutes(TIMEOUT_MINUTES + 30)).build()); + entityManager.flush(); entityManager.clear(); } @@ -120,6 +130,81 @@ class FindCurrentlyActiveByRoomNumber { } } + @Nested + @DisplayName("findCurrentlyActiveByMachineId 메서드는") + class FindCurrentlyActiveByMachineId { + + @Test + @DisplayName("만료되지 않은 활성 예약만 반환한다") + void 만료되지_않은_활성_예약만_반환한다() { + assertActiveIdsAre(() -> reservationRepository.findCurrentlyActiveByMachineId(washer.getId())); + } + + @Test + @DisplayName("만료 예약만 남은 기기에는 빈 목록을 반환한다") + void 만료_예약만_남은_기기에는_빈_목록을_반환한다() { + assertThat(reservationRepository.findCurrentlyActiveByMachineId(expiredOnlyMachine.getId())).isEmpty(); + } + } + + @Nested + @DisplayName("findCurrentlyActiveReservationByMachineId 메서드는") + class FindCurrentlyActiveReservationByMachineId { + + @Test + @DisplayName("RUNNING 예약을 대표 예약으로 반환한다") + void RUNNING_예약을_대표_예약으로_반환한다() { + assertThat(reservationRepository.findCurrentlyActiveReservationByMachineId(washer.getId())).get() + .extracting(Reservation::getId).isEqualTo(longRunning.getId()); + } + + @Test + @DisplayName("만료 예약만 남은 기기에는 빈 값을 반환한다") + void 만료_예약만_남은_기기에는_빈_값을_반환한다() { + assertThat(reservationRepository.findCurrentlyActiveReservationByMachineId(expiredOnlyMachine.getId())) + .isEmpty(); + } + } + + @Nested + @DisplayName("findCurrentlyActiveMachineIds 메서드는") + class FindCurrentlyActiveMachineIds { + + @Test + @DisplayName("만료 예약만 남은 기기를 제외한 기기 ID만 반환한다") + void 만료_예약만_남은_기기를_제외한_기기_ID만_반환한다() { + assertThat(reservationRepository.findCurrentlyActiveMachineIds()).containsExactly(washer.getId()); + } + } + + @Nested + @DisplayName("countCurrentlyActive 메서드는") + class CountCurrentlyActive { + + @Test + @DisplayName("만료 예약을 제외한 활성 예약 수를 반환한다") + void 만료_예약을_제외한_활성_예약_수를_반환한다() { + assertThat(reservationRepository.countCurrentlyActive()).isEqualTo(4L); + } + } + + @Nested + @DisplayName("existsCurrentlyActiveByUser 메서드는") + class ExistsCurrentlyActiveByUser { + + @Test + @DisplayName("만료되지 않은 활성 예약이 있으면 참을 반환한다") + void 만료되지_않은_활성_예약이_있으면_참을_반환한다() { + assertThat(reservationRepository.existsCurrentlyActiveByUser(owner)).isTrue(); + } + + @Test + @DisplayName("만료 예약만 남은 사용자에게는 거짓을 반환한다") + void 만료_예약만_남은_사용자에게는_거짓을_반환한다() { + assertThat(reservationRepository.existsCurrentlyActiveByUser(expiredOnlyOwner)).isFalse(); + } + } + @Nested @DisplayName("쿼리 조건과 엔티티 판정은") class QueryAndEntityRule { diff --git a/src/test/java/team/washer/server/v2/domain/smartthings/service/ShutdownIdleMachinesServiceTest.java b/src/test/java/team/washer/server/v2/domain/smartthings/service/ShutdownIdleMachinesServiceTest.java index a48e758e..9fd35bd5 100644 --- a/src/test/java/team/washer/server/v2/domain/smartthings/service/ShutdownIdleMachinesServiceTest.java +++ b/src/test/java/team/washer/server/v2/domain/smartthings/service/ShutdownIdleMachinesServiceTest.java @@ -23,7 +23,6 @@ import team.washer.server.v2.domain.machine.enums.MachineType; import team.washer.server.v2.domain.machine.enums.Position; import team.washer.server.v2.domain.machine.repository.MachineRepository; -import team.washer.server.v2.domain.reservation.enums.ReservationStatus; import team.washer.server.v2.domain.reservation.repository.ReservationRepository; import team.washer.server.v2.domain.smartthings.dto.response.SmartThingsDeviceStatusResDto; import team.washer.server.v2.domain.smartthings.exception.SmartThingsPermissionException; @@ -51,9 +50,6 @@ class ShutdownIdleMachinesServiceTest { @Mock private DeviceShutdownSupport deviceShutdownSupport; - private static final List ACTIVE_STATUSES = List.of(ReservationStatus.RESERVED, - ReservationStatus.RUNNING); - private static final SmartThingsDeviceStatusResDto EMPTY_STATUS = new SmartThingsDeviceStatusResDto(Map.of()); private Machine createMachine(final Long id, final String name, final String deviceId) { @@ -97,7 +93,7 @@ void it_delegates_safe_shutdown() { // Given var machine = createMachine(1L, "W-2F-L1", "device-1"); given(machineRepository.findAll()).willReturn(List.of(machine)); - given(reservationRepository.findMachineIdsByStatusIn(ACTIVE_STATUSES)).willReturn(List.of()); + given(reservationRepository.findCurrentlyActiveMachineIds()).willReturn(List.of()); given(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1"))) .willReturn(Map.of("device-1", EMPTY_STATUS)); given(deviceShutdownSupport.shutdown(eq(machine), any())).willReturn(ShutdownResult.POWERED_OFF); @@ -120,7 +116,7 @@ void it_skips_reserved_machine() { // Given var machine = createMachine(1L, "W-2F-L1", "device-1"); given(machineRepository.findAll()).willReturn(List.of(machine)); - given(reservationRepository.findMachineIdsByStatusIn(ACTIVE_STATUSES)).willReturn(List.of(1L)); + given(reservationRepository.findCurrentlyActiveMachineIds()).willReturn(List.of(1L)); // When shutdownIdleMachinesService.execute(); @@ -141,7 +137,7 @@ void it_safely_stops_without_power_off() { // Given var machine = createMachine(1L, "W-2F-L1", "device-1"); given(machineRepository.findAll()).willReturn(List.of(machine)); - given(reservationRepository.findMachineIdsByStatusIn(ACTIVE_STATUSES)).willReturn(List.of()); + given(reservationRepository.findCurrentlyActiveMachineIds()).willReturn(List.of()); given(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1"))) .willReturn(Map.of("device-1", EMPTY_STATUS)); given(deviceShutdownSupport.shutdown(eq(machine), any())).willReturn(ShutdownResult.STOPPED); @@ -165,7 +161,7 @@ void it_stops_batch_on_permission_error() { var machine1 = createMachine(1L, "W-2F-L1", "device-1"); var machine2 = createMachine(2L, "W-2F-R1", "device-2"); given(machineRepository.findAll()).willReturn(List.of(machine1, machine2)); - given(reservationRepository.findMachineIdsByStatusIn(ACTIVE_STATUSES)).willReturn(List.of()); + given(reservationRepository.findCurrentlyActiveMachineIds()).willReturn(List.of()); given(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1", "device-2"))) .willReturn(Map.of("device-1", EMPTY_STATUS, "device-2", EMPTY_STATUS)); willThrow(new SmartThingsPermissionException("권한 없음")).given(deviceShutdownSupport) @@ -191,7 +187,7 @@ void it_continues_processing_after_failure() { var machine1 = createMachine(1L, "W-2F-L1", "device-1"); var machine2 = createMachine(2L, "W-2F-R1", "device-2"); given(machineRepository.findAll()).willReturn(List.of(machine1, machine2)); - given(reservationRepository.findMachineIdsByStatusIn(ACTIVE_STATUSES)).willReturn(List.of()); + given(reservationRepository.findCurrentlyActiveMachineIds()).willReturn(List.of()); given(deviceStatusQuerySupport.queryAllDevicesStatus(List.of("device-1", "device-2"))) .willReturn(Map.of("device-1", EMPTY_STATUS, "device-2", EMPTY_STATUS)); willThrow(new RuntimeException("일시적 오류")).given(deviceShutdownSupport).shutdown(eq(machine1), any()); diff --git a/src/test/java/team/washer/server/v2/domain/user/service/DeleteUserServiceTest.java b/src/test/java/team/washer/server/v2/domain/user/service/DeleteUserServiceTest.java index 81926120..b4f8f5a2 100644 --- a/src/test/java/team/washer/server/v2/domain/user/service/DeleteUserServiceTest.java +++ b/src/test/java/team/washer/server/v2/domain/user/service/DeleteUserServiceTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; -import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -16,7 +15,6 @@ import org.springframework.http.HttpStatus; import team.themoment.sdk.exception.ExpectedException; -import team.washer.server.v2.domain.reservation.enums.ReservationStatus; import team.washer.server.v2.domain.reservation.repository.ReservationRepository; import team.washer.server.v2.domain.user.entity.User; import team.washer.server.v2.domain.user.repository.UserRepository; @@ -54,17 +52,15 @@ void it_deletes_user() { // Given Long userId = 1L; User user = createUser(); - List activeStatuses = List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING); - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - given(reservationRepository.existsByUserAndStatusIn(user, activeStatuses)).willReturn(false); + given(reservationRepository.existsCurrentlyActiveByUser(user)).willReturn(false); // When deleteUserService.execute(userId); // Then then(userRepository).should(times(1)).findById(userId); - then(reservationRepository).should(times(1)).existsByUserAndStatusIn(user, activeStatuses); + then(reservationRepository).should(times(1)).existsCurrentlyActiveByUser(user); then(userRepository).should(times(1)).delete(user); } } @@ -79,10 +75,8 @@ void it_throws_expected_exception() { // Given Long userId = 1L; User user = createUser(); - List activeStatuses = List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING); - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - given(reservationRepository.existsByUserAndStatusIn(user, activeStatuses)).willReturn(true); + given(reservationRepository.existsCurrentlyActiveByUser(user)).willReturn(true); // When & Then assertThatThrownBy(() -> deleteUserService.execute(userId)).isInstanceOf(ExpectedException.class) @@ -90,7 +84,7 @@ void it_throws_expected_exception() { .hasFieldOrPropertyWithValue("statusCode", HttpStatus.BAD_REQUEST); then(userRepository).should(times(1)).findById(userId); - then(reservationRepository).should(times(1)).existsByUserAndStatusIn(user, activeStatuses); + then(reservationRepository).should(times(1)).existsCurrentlyActiveByUser(user); then(userRepository).should(never()).delete(any(User.class)); } } @@ -105,10 +99,8 @@ void it_throws_expected_exception() { // Given Long userId = 1L; User user = createUser(); - List activeStatuses = List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING); - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - given(reservationRepository.existsByUserAndStatusIn(user, activeStatuses)).willReturn(true); + given(reservationRepository.existsCurrentlyActiveByUser(user)).willReturn(true); // When & Then assertThatThrownBy(() -> deleteUserService.execute(userId)).isInstanceOf(ExpectedException.class) @@ -116,7 +108,7 @@ void it_throws_expected_exception() { .hasFieldOrPropertyWithValue("statusCode", HttpStatus.BAD_REQUEST); then(userRepository).should(times(1)).findById(userId); - then(reservationRepository).should(times(1)).existsByUserAndStatusIn(user, activeStatuses); + then(reservationRepository).should(times(1)).existsCurrentlyActiveByUser(user); then(userRepository).should(never()).delete(any(User.class)); } } @@ -131,17 +123,15 @@ void it_deletes_user() { // Given Long userId = 1L; User user = createUser(); - List activeStatuses = List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING); - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - given(reservationRepository.existsByUserAndStatusIn(user, activeStatuses)).willReturn(false); + given(reservationRepository.existsCurrentlyActiveByUser(user)).willReturn(false); // When deleteUserService.execute(userId); // Then then(userRepository).should(times(1)).findById(userId); - then(reservationRepository).should(times(1)).existsByUserAndStatusIn(user, activeStatuses); + then(reservationRepository).should(times(1)).existsCurrentlyActiveByUser(user); then(userRepository).should(times(1)).delete(user); } } @@ -156,17 +146,38 @@ void it_deletes_user() { // Given Long userId = 1L; User user = createUser(); - List activeStatuses = List.of(ReservationStatus.RESERVED, ReservationStatus.RUNNING); - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - given(reservationRepository.existsByUserAndStatusIn(user, activeStatuses)).willReturn(false); + given(reservationRepository.existsCurrentlyActiveByUser(user)).willReturn(false); // When deleteUserService.execute(userId); // Then then(userRepository).should(times(1)).findById(userId); - then(reservationRepository).should(times(1)).existsByUserAndStatusIn(user, activeStatuses); + then(reservationRepository).should(times(1)).existsCurrentlyActiveByUser(user); + then(userRepository).should(times(1)).delete(user); + } + } + + @Nested + @DisplayName("타임아웃이 지난 RESERVED 예약만 남은 사용자를 삭제할 때") + class Context_with_expired_reservations_only { + + @Test + @DisplayName("사용자를 삭제해야 한다") + void it_deletes_user() { + // Given + Long userId = 1L; + User user = createUser(); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(reservationRepository.existsCurrentlyActiveByUser(user)).willReturn(false); + + // When + deleteUserService.execute(userId); + + // Then + then(reservationRepository).should(times(1)).existsCurrentlyActiveByUser(user); then(userRepository).should(times(1)).delete(user); } } @@ -188,7 +199,7 @@ void it_throws_expected_exception() { .hasMessage("사용자를 찾을 수 없습니다").hasFieldOrPropertyWithValue("statusCode", HttpStatus.NOT_FOUND); then(userRepository).should(times(1)).findById(userId); - then(reservationRepository).should(never()).existsByUserAndStatusIn(any(User.class), anyList()); + then(reservationRepository).should(never()).existsCurrentlyActiveByUser(any(User.class)); then(userRepository).should(never()).delete(any(User.class)); } }