Skip to content

Commit

Permalink
feat: 스케쥴러 구현 (#63)
Browse files Browse the repository at this point in the history
* feat: 시험장 예약 스케쥴링 구현

* feat: 차량 예약 스케쥴링 구현

* feat: 시험장 자동반납 스케쥴러 구현
  • Loading branch information
gengminy authored Dec 17, 2023
1 parent d38ca12 commit 163d5ef
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 10 deletions.
10 changes: 10 additions & 0 deletions src/main/java/com/testcar/car/config/SchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.testcar.car.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

/** 스프링 스케쥴러를 활성화하는 설정 빈 */
@Configuration
@EnableScheduling
public class SchedulerConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.testcar.car.domains.carReservation;


import com.testcar.car.domains.carReservation.entity.CarReservation;
import com.testcar.car.domains.carReservation.repository.CarReservationRepository;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/** 차량 예약에 대한 스케쥴링 설정 */
@Service
@Transactional
@RequiredArgsConstructor
public class CarReservationSchedulerService {
private final CarReservationRepository carReservationRepository;

/** 매일 자정 예약중인 차량의 반납시간이 도래하면 이용완료로 변경한다. */
@Scheduled(cron = "0 0 0 * * ?")
public void updateAllReturned() {
// 각 차량 예약 건의 expiredAt 시간이 now 와 같다면 이용완료 시킨다.
final LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0);
final List<CarReservation> carReservations =
carReservationRepository.findAllByExpiredAtAndStatusReserved(now);

carReservations.forEach(CarReservation::updateReturn);
carReservationRepository.saveAll(carReservations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.testcar.car.domains.carReservation.entity.CarReservation;
import com.testcar.car.domains.carReservation.model.dto.CarReservationDto;
import com.testcar.car.domains.carReservation.model.vo.CarReservationFilterCondition;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -13,4 +14,6 @@ Page<CarReservationDto> findAllPageByCondition(
CarReservationFilterCondition condition, Pageable pageable);

List<CarReservation> findAllWithCarStockByIdInAndMemberId(List<Long> ids, Long memberId);

List<CarReservation> findAllByExpiredAtAndStatusReserved(LocalDateTime expiredAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.testcar.car.domains.carReservation.entity.ReservationStatus;
import com.testcar.car.domains.carReservation.model.dto.CarReservationDto;
import com.testcar.car.domains.carReservation.model.vo.CarReservationFilterCondition;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -72,6 +73,19 @@ public List<CarReservation> findAllWithCarStockByIdInAndMemberId(
.fetch();
}

@Override
public List<CarReservation> findAllByExpiredAtAndStatusReserved(LocalDateTime expiredAt) {
return jpaQueryFactory
.selectFrom(carReservation)
.leftJoin(carReservation.carStock, carStock)
.fetchJoin()
.where(
notDeleted(carReservation),
carReservation.expiredAt.eq(expiredAt),
carReservation.status.eq(ReservationStatus.RESERVED))
.fetch();
}

private BooleanExpression carNameContainsOrNull(String name) {
return name == null ? null : carReservation.carStock.car.name.contains(name);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.testcar.car.domains.trackReservation;


import com.testcar.car.domains.trackReservation.entity.TrackReservation;
import com.testcar.car.domains.trackReservation.entity.TrackReservationSlot;
import com.testcar.car.domains.trackReservation.repository.TrackReservationRepository;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/** 시험장 예약에 대한 스케쥴링 설정 */
@Service
@Transactional
@RequiredArgsConstructor
public class TrackReservationSchedulerService {
private final TrackReservationRepository trackReservationRepository;

/** 매 시간 예약중인 시험장의 반납시간이 도래하면 이용완료로 변경한다. */
@Scheduled(cron = "0 0 * * * ?")
public void updateAllCompleted() {
// 각 시험장 예약의 슬롯 중, 마지막 시간의 expiredAt 시간이 now 와 같다면 이용완료 시킨다.
final LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0);
final List<TrackReservation> trackReservations =
trackReservationRepository.findAllBySlotExpiredAtAndStatusReserved(now);

trackReservations.stream()
.filter(trackReservation -> isLastSlotExpiredAtEquals(trackReservation, now))
.forEach(TrackReservation::completed);
trackReservationRepository.saveAll(trackReservations);
}

private boolean isLastSlotExpiredAtEquals(
TrackReservation trackReservation, LocalDateTime time) {
return trackReservation.getTrackReservationSlots().stream()
.max(Comparator.comparing(TrackReservationSlot::getExpiredAt))
.map(TrackReservationSlot::getExpiredAt)
.filter(expiredAt -> expiredAt.equals(time))
.isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
@Getter
@RequiredArgsConstructor
public enum ReservationStatus {
RESERVED("예약완료"),
USING("사용중"),
RESERVED("예약중"),
CANCELED("예약취소"),
COMPLETED("이용완료");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ public void cancel() {
this.status = ReservationStatus.CANCELED;
}

public void completed() {
this.status = ReservationStatus.COMPLETED;
}

public boolean isCancelable() {
return this.status == ReservationStatus.RESERVED || this.status == ReservationStatus.USING;
return this.status == ReservationStatus.RESERVED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import com.testcar.car.domains.trackReservation.entity.TrackReservation;
import com.testcar.car.domains.trackReservation.model.vo.TrackReservationFilterCondition;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand All @@ -11,4 +12,6 @@ public interface TrackReservationCustomRepository {

List<TrackReservation> findAllByMemberIdAndCondition(
Long memberId, TrackReservationFilterCondition condition);

List<TrackReservation> findAllBySlotExpiredAtAndStatusReserved(LocalDateTime expiredAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ public List<TrackReservation> findAllByMemberIdAndCondition(
.fetch();
}

/** 슬롯의 expiredAt 이 같으면서 RESERVED 상태인 모든 예약을 가져옴 */
@Override
public List<TrackReservation> findAllBySlotExpiredAtAndStatusReserved(LocalDateTime expiredAt) {
return jpaQueryFactory
.selectFrom(trackReservation)
.leftJoin(trackReservation.track, track)
.fetchJoin()
.leftJoin(trackReservation.trackReservationSlots, trackReservationSlot)
.fetchJoin()
.where(
notDeleted(trackReservation),
trackReservation.trackReservationSlots.any().expiredAt.eq(expiredAt),
(trackReservation.status.eq(ReservationStatus.RESERVED)))
.fetch();
}

private BooleanExpression trackNameContainsOrNull(String name) {
return (name == null) ? null : track.name.contains(name);
}
Expand All @@ -76,13 +92,11 @@ private OrderSpecifier<?> orderByReservationStatus() {
.status
.when(ReservationStatus.RESERVED)
.then(1)
.when(ReservationStatus.USING)
.when(ReservationStatus.COMPLETED)
.then(2)
.when(ReservationStatus.CANCELED)
.then(3)
.when(ReservationStatus.COMPLETED)
.then(4)
.otherwise(5)
.otherwise(4)
.asc();
}
}
2 changes: 1 addition & 1 deletion src/test/java/com/testcar/car/common/Constant.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ private Constant() {}

/** Date */
public static final LocalDateTime NOW =
LocalDateTime.now().withHour(12).withMinute(0).withSecond(0).withNano(0);
LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);

public static final LocalDateTime TOMORROW = NOW.plusDays(1L);
public static final LocalDateTime DAY_AFTER_TOMORROW = NOW.plusDays(2L);
Expand Down
15 changes: 15 additions & 0 deletions src/test/java/com/testcar/car/common/TrackEntityFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.testcar.car.domains.trackReservation.entity.TrackReservation;
import com.testcar.car.domains.trackReservation.entity.TrackReservation.TrackReservationBuilder;
import com.testcar.car.domains.trackReservation.entity.TrackReservationSlot;
import java.time.LocalDateTime;
import java.util.Set;

public class TrackEntityFactory {
Expand Down Expand Up @@ -56,6 +57,15 @@ public static TrackReservationSlot createTrackReservationSlot(
.build();
}

public static TrackReservationSlot createTrackReservationSlot(
TrackReservation trackReservation, LocalDateTime expiredAt) {
return createTrackReservationSlotBuilder()
.trackReservation(trackReservation)
.startedAt(expiredAt.minusHours(1L))
.expiredAt(expiredAt)
.build();
}

public static TrackReservationSlot createAnotherTrackReservationSlot(
TrackReservation trackReservation) {
return createTrackReservationSlotBuilder()
Expand All @@ -75,6 +85,11 @@ public static Set<TrackReservationSlot> createTrackReservationSlotSet(
return Set.of(createTrackReservationSlot(trackReservation));
}

public static Set<TrackReservationSlot> createTrackReservationSlotSet(
TrackReservation trackReservation, LocalDateTime expiredAt) {
return Set.of(createTrackReservationSlot(trackReservation, expiredAt));
}

public static Set<TrackReservationSlot> createAnotherTrackReservationSlotSet(
TrackReservation trackReservation) {
return Set.of(createAnotherTrackReservationSlot(trackReservation));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.testcar.car.domains.carReservation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

import com.testcar.car.common.CarEntityFactory;
import com.testcar.car.domains.carReservation.entity.CarReservation;
import com.testcar.car.domains.carReservation.entity.ReservationStatus;
import com.testcar.car.domains.carReservation.repository.CarReservationRepository;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class CarReservationSchedulerServiceTest {
@Mock private CarReservationRepository carReservationRepository;
@InjectMocks private CarReservationSchedulerService carReservationSchedulerService;

private static List<CarReservation> carReservations;

@BeforeAll
public static void setUp() {
carReservations =
List.of(
CarEntityFactory.createCarReservation(),
CarEntityFactory.createCarReservation(),
CarEntityFactory.createCarReservation());
}

@Test
void 사용중인_차량예약_마감시간이_되면_모두_반납한다() {
// given
final LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0);
given(carReservationRepository.findAllByExpiredAtAndStatusReserved(now))
.willReturn(carReservations.subList(1, 2));

// when
carReservationSchedulerService.updateAllReturned();

// then
assertEquals(ReservationStatus.RESERVED, carReservations.get(0).getStatus());
assertEquals(ReservationStatus.RETURNED, carReservations.get(1).getStatus());
assertEquals(ReservationStatus.RESERVED, carReservations.get(2).getStatus());
then(carReservationRepository).should().findAllByExpiredAtAndStatusReserved(now);
then(carReservationRepository).should().saveAll(carReservations.subList(1, 2));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.testcar.car.domains.trackReservation;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.testcar.car.domains.trackReservation.entity.TrackReservation;
import com.testcar.car.domains.trackReservation.entity.TrackReservationSlot;
import com.testcar.car.domains.trackReservation.repository.TrackReservationRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class TrackReservationSchedulerServiceTest {
@Mock private TrackReservationRepository trackReservationRepository;
@InjectMocks private TrackReservationSchedulerService trackReservationSchedulerService;

private TrackReservation todayTrackReservation;
private TrackReservation tomorrowTrackReservation;
private static final LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0);

@BeforeEach
public void setUp() {
todayTrackReservation = mock(TrackReservation.class);
TrackReservationSlot slot = mock(TrackReservationSlot.class);
when(todayTrackReservation.getTrackReservationSlots()).thenReturn(Set.of(slot));
when(slot.getExpiredAt()).thenReturn(now);
doCallRealMethod().when(todayTrackReservation).completed();

tomorrowTrackReservation = mock(TrackReservation.class);
}

@Test
void 사용중인_시험장_마지막_슬롯의_마감시간이_도래하면_시험장을_반납한다() {
// given
List<TrackReservation> reservations =
List.of(todayTrackReservation, tomorrowTrackReservation);
when(trackReservationRepository.findAllBySlotExpiredAtAndStatusReserved(
any(LocalDateTime.class)))
.thenReturn(reservations);
when(trackReservationRepository.saveAll(reservations)).thenReturn(reservations);

// when
trackReservationSchedulerService.updateAllCompleted();

// then
verify(todayTrackReservation).completed();
verify(tomorrowTrackReservation, never()).completed();
verify(trackReservationRepository).findAllBySlotExpiredAtAndStatusReserved(now);
verify(trackReservationRepository).saveAll(reservations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public static void setUp() {
@ParameterizedTest
@EnumSource(
value = ReservationStatus.class,
names = {"RESERVED", "USING"})
names = {"RESERVED"})
void 시험장_예약을_취소_또는_반납한다(ReservationStatus status) {
// given
final TrackReservation cancelableTrackReservation =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class TrackReservationTest {
@ParameterizedTest
@EnumSource(
value = ReservationStatus.class,
names = {"RESERVED", "USING"})
names = {"RESERVED"})
public void 시험장을_취소할수_있는지_확인한다(ReservationStatus status) {
// given
final TrackReservation trackReservation =
Expand Down

0 comments on commit 163d5ef

Please sign in to comment.