diff --git a/build.gradle b/build.gradle index 57f64ad1..361994f5 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,8 @@ dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/com/prgrms/catchtable/common/exception/ErrorCode.java b/src/main/java/com/prgrms/catchtable/common/exception/ErrorCode.java index 876f8b79..02c9e311 100644 --- a/src/main/java/com/prgrms/catchtable/common/exception/ErrorCode.java +++ b/src/main/java/com/prgrms/catchtable/common/exception/ErrorCode.java @@ -6,7 +6,11 @@ @Getter @RequiredArgsConstructor public enum ErrorCode { - NOT_EXIST_MEMBER("존재하지 않는 아이디입니다."); + NOT_EXIST_MEMBER("존재하지 않는 아이디입니다."), + ALREADY_PREOCCUPIED_RESERVATION_TIME("이미 타인에게 선점권이 있는 예약시간입니다."), + ALREADY_OCCUPIED_RESERVATION_TIME("이미 예약된 시간입니다."), + NOT_EXIST_SHOP("존재하지 않는 매장입니다."), + NOT_EXIST_TIME("존재하지 않는 예약 시간입니다."); private final String message; } diff --git a/src/main/java/com/prgrms/catchtable/facade/ReservationFacade.java b/src/main/java/com/prgrms/catchtable/facade/ReservationFacade.java new file mode 100644 index 00000000..a00172a5 --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/facade/ReservationFacade.java @@ -0,0 +1,35 @@ +package com.prgrms.catchtable.facade; + +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import com.prgrms.catchtable.reservation.dto.request.CreateReservationRequest; +import com.prgrms.catchtable.reservation.dto.response.CreateReservationResponse; +import com.prgrms.catchtable.reservation.service.ReservationAsync; +import com.prgrms.catchtable.reservation.service.ReservationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReservationFacade { + + private final ReservationService reservationService; + private final ReservationAsync reservationAsync; + + public CreateReservationResponse preOccupyReservation( + CreateReservationRequest request) { + ReservationTime reservationTime = reservationService.validateReservationAndSave( + request); + + String shopName = reservationTime.getShop().getName(); + + reservationAsync.setPreOcuppied(reservationTime); + + return CreateReservationResponse.builder() + .shopName(shopName) + .memberName("memberA") + .date(reservationTime.getTime()) + .peopleCount(request.peopleCount()) + .build(); + } + +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/controller/ReservationController.java b/src/main/java/com/prgrms/catchtable/reservation/controller/ReservationController.java new file mode 100644 index 00000000..8f344697 --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/controller/ReservationController.java @@ -0,0 +1,24 @@ +package com.prgrms.catchtable.reservation.controller; + +import com.prgrms.catchtable.facade.ReservationFacade; +import com.prgrms.catchtable.reservation.dto.request.CreateReservationRequest; +import com.prgrms.catchtable.reservation.dto.response.CreateReservationResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/reservations") +@RequiredArgsConstructor +public class ReservationController { + + private final ReservationFacade reservationFacade; + + @PostMapping + public ResponseEntity createReservationResponse( + CreateReservationRequest request) { + return ResponseEntity.ok(reservationFacade.preOccupyReservation(request)); + } +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/domain/Reservation.java b/src/main/java/com/prgrms/catchtable/reservation/domain/Reservation.java index f4e47763..db66a48b 100644 --- a/src/main/java/com/prgrms/catchtable/reservation/domain/Reservation.java +++ b/src/main/java/com/prgrms/catchtable/reservation/domain/Reservation.java @@ -49,11 +49,15 @@ public class Reservation extends BaseEntity { @OneToOne(fetch = LAZY) @JoinColumn(name = "reservation_time_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) - private ReservationTime time; + private ReservationTime reservationTime; @Builder - public Reservation(ReservationStatus status, int peopleCount) { + public Reservation(ReservationStatus status, int peopleCount, Shop shop, + ReservationTime reservationTime) { this.status = status; this.peopleCount = peopleCount; + this.shop = shop; + this.reservationTime = reservationTime; } + } diff --git a/src/main/java/com/prgrms/catchtable/reservation/domain/ReservationTime.java b/src/main/java/com/prgrms/catchtable/reservation/domain/ReservationTime.java index 60ee4924..8b4f6e43 100644 --- a/src/main/java/com/prgrms/catchtable/reservation/domain/ReservationTime.java +++ b/src/main/java/com/prgrms/catchtable/reservation/domain/ReservationTime.java @@ -34,6 +34,9 @@ public class ReservationTime { @Column(name = "is_occupied") private boolean isOccupied; + @Column(name = "is_pre_occupied") + private boolean isPreOccupied; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "shop_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Shop shop; @@ -43,4 +46,16 @@ public ReservationTime(LocalDateTime time) { this.time = time; this.isOccupied = false; } + + public void reverseOccupied() { + this.isOccupied = !this.isOccupied; + } + + public void reversePreOccupied() { + this.isPreOccupied = !this.isPreOccupied; + } + + public void insertShop(Shop shop) { + this.shop = shop; + } } diff --git a/src/main/java/com/prgrms/catchtable/reservation/dto/request/CreateReservationRequest.java b/src/main/java/com/prgrms/catchtable/reservation/dto/request/CreateReservationRequest.java new file mode 100644 index 00000000..94ec2b44 --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/dto/request/CreateReservationRequest.java @@ -0,0 +1,9 @@ +package com.prgrms.catchtable.reservation.dto.request; + +import lombok.Builder; + +@Builder +public record CreateReservationRequest(Long reservationTimeId, + int peopleCount) { + +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/dto/response/CreateReservationResponse.java b/src/main/java/com/prgrms/catchtable/reservation/dto/response/CreateReservationResponse.java new file mode 100644 index 00000000..fb871d6f --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/dto/response/CreateReservationResponse.java @@ -0,0 +1,12 @@ +package com.prgrms.catchtable.reservation.dto.response; + +import java.time.LocalDateTime; +import lombok.Builder; + +@Builder +public record CreateReservationResponse(String shopName, + String memberName, + LocalDateTime date, + int peopleCount) { + +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/repository/ReservationRepository.java b/src/main/java/com/prgrms/catchtable/reservation/repository/ReservationRepository.java new file mode 100644 index 00000000..e4e2642a --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/repository/ReservationRepository.java @@ -0,0 +1,8 @@ +package com.prgrms.catchtable.reservation.repository; + +import com.prgrms.catchtable.reservation.domain.Reservation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReservationRepository extends JpaRepository { + +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/repository/ReservationTimeRepository.java b/src/main/java/com/prgrms/catchtable/reservation/repository/ReservationTimeRepository.java new file mode 100644 index 00000000..63ae522a --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/repository/ReservationTimeRepository.java @@ -0,0 +1,8 @@ +package com.prgrms.catchtable.reservation.repository; + +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReservationTimeRepository extends JpaRepository { + +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/service/ReservationAsync.java b/src/main/java/com/prgrms/catchtable/reservation/service/ReservationAsync.java new file mode 100644 index 00000000..c9a1e897 --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/service/ReservationAsync.java @@ -0,0 +1,24 @@ +package com.prgrms.catchtable.reservation.service; + +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class ReservationAsync { + + @Transactional + public void setPreOcuppied(ReservationTime reservationTime) { + reservationTime.reversePreOccupied(); + + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(reservationTime::reversePreOccupied, 10, TimeUnit.SECONDS); + + scheduler.shutdown(); + } +} diff --git a/src/main/java/com/prgrms/catchtable/reservation/service/ReservationService.java b/src/main/java/com/prgrms/catchtable/reservation/service/ReservationService.java new file mode 100644 index 00000000..343a1293 --- /dev/null +++ b/src/main/java/com/prgrms/catchtable/reservation/service/ReservationService.java @@ -0,0 +1,33 @@ +package com.prgrms.catchtable.reservation.service; + +import static com.prgrms.catchtable.common.exception.ErrorCode.ALREADY_PREOCCUPIED_RESERVATION_TIME; +import static com.prgrms.catchtable.common.exception.ErrorCode.NOT_EXIST_TIME; + +import com.prgrms.catchtable.common.exception.custom.BadRequestCustomException; +import com.prgrms.catchtable.common.exception.custom.NotFoundCustomException; +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import com.prgrms.catchtable.reservation.dto.request.CreateReservationRequest; +import com.prgrms.catchtable.reservation.repository.ReservationTimeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationTimeRepository reservationTimeRepository; + + @Transactional + public ReservationTime validateReservationAndSave(CreateReservationRequest request) { + ReservationTime reservationTime = reservationTimeRepository.findById( + request.reservationTimeId()) + .orElseThrow(() -> new NotFoundCustomException(NOT_EXIST_TIME)); + + if (reservationTime.isPreOccupied()) { + throw new BadRequestCustomException(ALREADY_PREOCCUPIED_RESERVATION_TIME); + } + + return reservationTime; + } +} diff --git a/src/test/java/com/prgrms/catchtable/common/data/reservation/ReservationData.java b/src/test/java/com/prgrms/catchtable/common/data/reservation/ReservationData.java new file mode 100644 index 00000000..f47d9941 --- /dev/null +++ b/src/test/java/com/prgrms/catchtable/common/data/reservation/ReservationData.java @@ -0,0 +1,45 @@ +package com.prgrms.catchtable.common.data.reservation; + +import static com.prgrms.catchtable.reservation.domain.ReservationStatus.COMPLETED; + +import com.prgrms.catchtable.common.data.shop.ShopData; +import com.prgrms.catchtable.reservation.domain.Reservation; +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import com.prgrms.catchtable.reservation.dto.request.CreateReservationRequest; +import java.time.LocalDateTime; + +public class ReservationData { + + public static Reservation getReservation() { + return Reservation.builder() + .status(COMPLETED) + .peopleCount(4) + .reservationTime(getReservationTimeNotPreOccupied()) + .build(); + } + + public static ReservationTime getReservationTimeNotPreOccupied() { + ReservationTime reservationTime = ReservationTime.builder() + .time(LocalDateTime.of(2024, 12, 31, 19, 30)) + .build(); + reservationTime.insertShop(ShopData.getShop()); + return reservationTime; + } + + public static ReservationTime getReservationTimePreOccupied() { + ReservationTime reservationTime = ReservationTime.builder() + .time(LocalDateTime.of(2024, 12, 31, 19, 30)) + .build(); + reservationTime.insertShop(ShopData.getShop()); + reservationTime.reversePreOccupied(); + return reservationTime; + } + + public static CreateReservationRequest getCreateReservationRequest() { + return CreateReservationRequest.builder() + .reservationTimeId(1L) + .peopleCount(4) + .build(); + } + +} diff --git a/src/test/java/com/prgrms/catchtable/common/data/shop/ShopData.java b/src/test/java/com/prgrms/catchtable/common/data/shop/ShopData.java new file mode 100644 index 00000000..4b71a7d1 --- /dev/null +++ b/src/test/java/com/prgrms/catchtable/common/data/shop/ShopData.java @@ -0,0 +1,20 @@ +package com.prgrms.catchtable.common.data.shop; + +import static com.prgrms.catchtable.shop.domain.Category.JAPANESE_FOOD; + +import com.prgrms.catchtable.shop.domain.Address; +import com.prgrms.catchtable.shop.domain.Shop; +import java.math.BigDecimal; + +public class ShopData { + + public static Shop getShop() { + return Shop.builder() + .name("shopA") + .rating(BigDecimal.valueOf(5L)) + .category(JAPANESE_FOOD) + .address(Address.builder().build()) + .capacity(30) + .build(); + } +} diff --git a/src/test/java/com/prgrms/catchtable/facade/ReservationFacadeTest.java b/src/test/java/com/prgrms/catchtable/facade/ReservationFacadeTest.java new file mode 100644 index 00000000..5323b921 --- /dev/null +++ b/src/test/java/com/prgrms/catchtable/facade/ReservationFacadeTest.java @@ -0,0 +1,48 @@ +package com.prgrms.catchtable.facade; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.prgrms.catchtable.common.data.reservation.ReservationData; +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import com.prgrms.catchtable.reservation.dto.request.CreateReservationRequest; +import com.prgrms.catchtable.reservation.dto.response.CreateReservationResponse; +import com.prgrms.catchtable.reservation.service.ReservationAsync; +import com.prgrms.catchtable.reservation.service.ReservationService; +import org.junit.jupiter.api.DisplayName; +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) +class ReservationFacadeTest { + + @Mock + private ReservationAsync reservationAsync; + @Mock + private ReservationService reservationService; + @InjectMocks + private ReservationFacade reservationFacade; + + @Test + @DisplayName("예약을 검증하고 선점권을 true로 바꾸는 것에 성공한다.") + void preOccupyReservation() { + ReservationTime reservationTime = ReservationData.getReservationTimeNotPreOccupied(); + CreateReservationRequest request = ReservationData.getCreateReservationRequest(); + + when(reservationService.validateReservationAndSave( + any(CreateReservationRequest.class))).thenReturn(reservationTime); + + CreateReservationResponse response = reservationFacade.preOccupyReservation( + request); + + assertAll( + () -> assertThat(response.date()).isEqualTo(reservationTime.getTime()), + () -> assertThat(response.peopleCount()).isEqualTo(request.peopleCount()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/catchtable/reservation/domain/ReservationTimeTest.java b/src/test/java/com/prgrms/catchtable/reservation/domain/ReservationTimeTest.java new file mode 100644 index 00000000..d8ff901d --- /dev/null +++ b/src/test/java/com/prgrms/catchtable/reservation/domain/ReservationTimeTest.java @@ -0,0 +1,28 @@ +package com.prgrms.catchtable.reservation.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.prgrms.catchtable.common.data.reservation.ReservationData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReservationTimeTest { + + @Test + @DisplayName("예약 선점 여부 변경에 성공한다") + void reversePreOccupied() { + ReservationTime reservationTime = ReservationData.getReservationTimeNotPreOccupied(); + reservationTime.reversePreOccupied(); + + assertThat(reservationTime.isPreOccupied()).isTrue(); + } + + @Test + @DisplayName("예약 여부 변경에 성공한다.") + void reverseOccupied() { + ReservationTime reservationTime = ReservationData.getReservationTimeNotPreOccupied(); + reservationTime.reverseOccupied(); + + assertThat(reservationTime.isOccupied()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/catchtable/reservation/service/ReservationServiceTest.java b/src/test/java/com/prgrms/catchtable/reservation/service/ReservationServiceTest.java new file mode 100644 index 00000000..80d65ee7 --- /dev/null +++ b/src/test/java/com/prgrms/catchtable/reservation/service/ReservationServiceTest.java @@ -0,0 +1,69 @@ +package com.prgrms.catchtable.reservation.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import com.prgrms.catchtable.common.data.reservation.ReservationData; +import com.prgrms.catchtable.common.exception.custom.BadRequestCustomException; +import com.prgrms.catchtable.reservation.domain.ReservationTime; +import com.prgrms.catchtable.reservation.dto.request.CreateReservationRequest; +import com.prgrms.catchtable.reservation.repository.ReservationTimeRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +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; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class ReservationServiceTest { + + @Mock + private ReservationTimeRepository reservationTimeRepository; + @InjectMocks + private ReservationService reservationService; + + @Test + @DisplayName("예약시간의 선점 여부를 검증하고 선점권이 빈 것을 확인한다.") + void validateReservation() { + //given + CreateReservationRequest request = ReservationData.getCreateReservationRequest(); + ReservationTime reservationTime = ReservationData.getReservationTimeNotPreOccupied(); + ReflectionTestUtils.setField(reservationTime, "id", 1L); + + when(reservationTimeRepository.findById(1L)).thenReturn(Optional.of(reservationTime)); + + //when + ReservationTime savedReservationTime = reservationService.validateReservationAndSave( + request); + + //then + assertAll( + () -> assertThat(savedReservationTime.getTime()).isEqualTo(reservationTime.getTime()), + () -> assertThat(savedReservationTime.getShop()).isEqualTo(reservationTime.getShop()) + ); + + + } + + @Test + @DisplayName("예약시간 선점권이 이미 타인에게 있는 경우 예외가 발생한다.") + void alreadyPreOccupied() { + //given + ReservationTime reservationTime = ReservationData.getReservationTimePreOccupied(); + CreateReservationRequest request = ReservationData.getCreateReservationRequest(); + ReflectionTestUtils.setField(reservationTime, "id", 1L); + + when(reservationTimeRepository.findById(1L)).thenReturn(Optional.of(reservationTime)); + + //when + assertThrows(BadRequestCustomException.class, + () -> reservationService.validateReservationAndSave(request)); + + + } +} \ No newline at end of file