From 48c754fd8e2eb969efe0ebb36a06f641689660be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Sat, 20 Apr 2024 22:01:45 +0900 Subject: [PATCH 01/21] =?UTF-8?q?[1=20-=203=EB=8B=A8=EA=B3=84=20-=20?= =?UTF-8?q?=EB=B0=A9=ED=83=88=EC=B6=9C=20=EC=98=88=EC=95=BD=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC]=20=EB=A6=AC=EB=B9=84(=EC=9D=B4=EA=B7=BC=ED=9D=AC)=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 커밋 메시지 템플릿 공동 작업자 설정 Co-authored-by: SeongjuLee * feat: "/admin" 요청 시 어드민 메인 페이지가 응답하는 기능 구현 Co-authored-by: SeongjuLee * test: admin 메인 페이지 응답 테스트 오류 수정 Co-authored-by: SeongjuLee * test: 예약 관리 페이지 응답 테스트 작성 Co-authored-by: SeongjuLee * feat: Reservation entity 구현 Co-authored-by: SeongjuLee * feat: 예약 관리 페이지 응답 기능 구현 Co-authored-by: SeongjuLee * test: 예약 추가 및 삭제 테스트 작성 Co-authored-by: SeongjuLee * feat: ReservationDto 구현 Co-authored-by: SeongjuLee * feat: 예약 추가 기능 구현 Co-authored-by: Seo현ngjuLee * feat: 예약 삭제 기능 구현 Co-authored-by: SeongjuLee * refactor: 관련된 컨트롤러 기능끼리 응집 개선 Co-authored-b현y: SeongjuLee * refactor: 컨트롤러 공통 리소스 계층화 개선 Co-authored-by: SeongjuLe선e * refactor: 컨트롤러 메서드 이름 가독성 개선 Co-authored-by: SeongjuL선ee * refactor: 주석 제거 개선 Co-authored-by: SeongjuL선ee * docs: 커밋 메시지 템플릿 삭제 * docs: API 명세서 작성 * refactor: RestController 응답 생성 시 ResponseEntity를 사용하도록 개선 * refactor: 컨트롤러 매핑 경로 개선 * style: dto 클래스명 구체화 개선 * feat: ReservationResponseDto 구현 * refactor: 컨트롤러 응답 시 Dto를 반환하도록 개선 * test: 테스트 코드 가독성 개선 * test: 테스트 클래스 분리 개선 * fix: Reservation, Time 클릭 시 url 요청 경로 수정 * test: 불필요한 테스트 컨테이너 격리 제거 개선 * refactor: 엔티티, DTO의 아이디 속성을 Wrapper 타입으로 변경 * docs: 도메인 명세 작성 * feat: Name 도메인 구현 * style: DisplayName 컨벤션 통일 (마침표 제거) * feat: ReservationTime 도메인 구현 * feat: Name 도메인 Getter 프로퍼티 추가 * feat: ReservationTime 도메인 Getter 프로퍼티 추가 * refactor: 엔티티가 포장 클래스를 이용, 비즈니스 로직을 담도록 개선 * feat: ReservationRepository 구현 * feat: 예약간 시간 충돌 확인 기능 구현 * refactor: Reservation 단건 조회 Optional 개선 및 deleteById 예외 상황 구현 * feat: 시간이 겹치는 예약이 있는지 조회하는 기능 구현 * feat: ReservationService 구현 * refactor: 레이어드 아키텍쳐 적용 개선 * refactor: DTO 클래스 이름 변경 개선 * refactor: requestDTO - toEntity 기능 구현 * refactor: Controller -> ViewController 클래스 이름 구체화 개선 * style: 코드 컨벤션 통일 개선 * refactor: 해당하는 Id 찾을 수 없음 예외 NoSuchElementException으로 변경 개선 * test: MemoryReservationRepositoryTest 작성 * chore: Mockito 의존성 추가 * test: 시간 겹침에 따른 예약 저장 실패, 성공 테스트 케이스 작성 * feat: ReservationTime NonNull검증 추가 구현 * refactor: 예외 메시지 구체화 개선 * docs: 기능 구현 목록 최신화 --------- Co-authored-by: SeongjuLee --- README.md | 21 +++++ build.gradle | 1 + .../roomescape/RoomescapeApplication.java | 1 - .../controller/AdminViewController.java | 19 +++++ .../controller/ReservationController.java | 46 +++++++++++ .../controller/dto/ReservationRequest.java | 36 ++++++++ .../controller/dto/ReservationResponse.java | 43 ++++++++++ src/main/java/roomescape/entity/Name.java | 35 ++++++++ .../java/roomescape/entity/Reservation.java | 41 ++++++++++ .../roomescape/entity/ReservationTime.java | 34 ++++++++ .../MemoryReservationRepository.java | 53 ++++++++++++ .../repository/ReservationRepository.java | 17 ++++ .../service/ReservationService.java | 31 +++++++ src/main/resources/templates/admin/index.html | 48 +++++------ .../templates/admin/reservation-legacy.html | 82 +++++++++---------- src/test/java/roomescape/MissionStepTest.java | 20 ----- .../controller/AdminViewControllerTest.java | 28 +++++++ .../controller/ReservationControllerTest.java | 59 +++++++++++++ src/test/java/roomescape/entity/NameTest.java | 34 ++++++++ .../roomescape/entity/ReservationTest.java | 33 ++++++++ .../entity/ReservationTimeTest.java | 41 ++++++++++ .../MemoryReservationRepositoryTest.java | 71 ++++++++++++++++ .../service/ReservationServiceTest.java | 48 +++++++++++ 23 files changed, 756 insertions(+), 86 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/roomescape/controller/AdminViewController.java create mode 100644 src/main/java/roomescape/controller/ReservationController.java create mode 100644 src/main/java/roomescape/controller/dto/ReservationRequest.java create mode 100644 src/main/java/roomescape/controller/dto/ReservationResponse.java create mode 100644 src/main/java/roomescape/entity/Name.java create mode 100644 src/main/java/roomescape/entity/Reservation.java create mode 100644 src/main/java/roomescape/entity/ReservationTime.java create mode 100644 src/main/java/roomescape/repository/MemoryReservationRepository.java create mode 100644 src/main/java/roomescape/repository/ReservationRepository.java create mode 100644 src/main/java/roomescape/service/ReservationService.java delete mode 100644 src/test/java/roomescape/MissionStepTest.java create mode 100644 src/test/java/roomescape/controller/AdminViewControllerTest.java create mode 100644 src/test/java/roomescape/controller/ReservationControllerTest.java create mode 100644 src/test/java/roomescape/entity/NameTest.java create mode 100644 src/test/java/roomescape/entity/ReservationTest.java create mode 100644 src/test/java/roomescape/entity/ReservationTimeTest.java create mode 100644 src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java create mode 100644 src/test/java/roomescape/service/ReservationServiceTest.java diff --git a/README.md b/README.md new file mode 100644 index 000000000..c6e91be22 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# 방탈출 API + +| Method | Endpoint | Description | +|--------|----------------------|--------------| +| GET | `/admin` | 어드민 페이지 요청 | +| GET | `/admin/reservation` | 예약 관리 페이지 요청 | +| GET | `/reservations` | 예약 정보 | +| POST | `/reservations` | 예약 추가 | +| DELETE | `/reservations/{id}` | 예약 취소 | + +# 방탈출 예약 도메인 명세 + +## Name + +- [x] 예약자 이름은 null이거나 빈 문자열일 수 없다. +- [x] 예약자 이름은 1~5사이의 길이를 가져야 한다. + +## ReservationTime + +- [x] 예약하려는 시간에 다른 예약이 존재한다면 예약이 불가능하다. +- [x] 예약 시간은 예약 시작 시간으로부터 한 시간의 길이를 가진다. diff --git a/build.gradle b/build.gradle index fdb3c9f06..12dc09e67 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ dependencies { runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.mockito:mockito-core:3.6.28' } test { diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 702706791..2ca0f743f 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -8,5 +8,4 @@ public class RoomescapeApplication { public static void main(String[] args) { SpringApplication.run(RoomescapeApplication.class, args); } - } diff --git a/src/main/java/roomescape/controller/AdminViewController.java b/src/main/java/roomescape/controller/AdminViewController.java new file mode 100644 index 000000000..8e918f1ef --- /dev/null +++ b/src/main/java/roomescape/controller/AdminViewController.java @@ -0,0 +1,19 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("/admin") +@Controller +public class AdminViewController { + @GetMapping() + public String showAdminMainPage() { + return "admin/index.html"; + } + + @GetMapping("/reservation") + public String showAdminReservationPage() { + return "admin/reservation-legacy"; + } +} diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java new file mode 100644 index 000000000..ff9c82c25 --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -0,0 +1,46 @@ +package roomescape.controller; + +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import roomescape.controller.dto.ReservationRequest; +import roomescape.controller.dto.ReservationResponse; +import roomescape.entity.Reservation; +import roomescape.service.ReservationService; + +@RequestMapping("/reservations") +@RestController +public class ReservationController { + private final ReservationService reservationService; + + public ReservationController(ReservationService reservationService) { + this.reservationService = reservationService; + } + + @GetMapping() + public ResponseEntity> readAllReservations() { + List reservations = reservationService.readAll() + .stream() + .map(ReservationResponse::from) + .toList(); + return ResponseEntity.ok().body(reservations); + } + + @PostMapping() + public ResponseEntity createReservation(@RequestBody ReservationRequest reservationRequest) { + Reservation savedReservation = reservationService.saveReservation(reservationRequest.toEntity()); + return ResponseEntity.ok().body(ReservationResponse.from(savedReservation)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteReservationById(@PathVariable("id") long id) { + reservationService.deleteReservation(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/roomescape/controller/dto/ReservationRequest.java b/src/main/java/roomescape/controller/dto/ReservationRequest.java new file mode 100644 index 000000000..636966108 --- /dev/null +++ b/src/main/java/roomescape/controller/dto/ReservationRequest.java @@ -0,0 +1,36 @@ +package roomescape.controller.dto; + +import java.time.LocalDate; +import java.time.LocalTime; +import roomescape.entity.Reservation; + +public class ReservationRequest { + private String name; + private LocalDate date; + private LocalTime time; + + public ReservationRequest() { + } + + public ReservationRequest(String name, LocalDate date, LocalTime time) { + this.name = name; + this.date = date; + this.time = time; + } + + public Reservation toEntity() { + return new Reservation(name, date, time); + } + + public String getName() { + return name; + } + + public LocalDate getDate() { + return date; + } + + public LocalTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/controller/dto/ReservationResponse.java b/src/main/java/roomescape/controller/dto/ReservationResponse.java new file mode 100644 index 000000000..d5bd0dc18 --- /dev/null +++ b/src/main/java/roomescape/controller/dto/ReservationResponse.java @@ -0,0 +1,43 @@ +package roomescape.controller.dto; + +import java.time.LocalDate; +import java.time.LocalTime; +import roomescape.entity.Reservation; + +public class ReservationResponse { + private final Long id; + private final String name; + private final LocalDate date; + private final LocalTime time; + + private ReservationResponse(long id, String name, LocalDate date, LocalTime time) { + this.id = id; + this.name = name; + this.date = date; + this.time = time; + } + + public static ReservationResponse from(Reservation reservation) { + return new ReservationResponse( + reservation.getId(), + reservation.getName(), + reservation.getStartDate(), + reservation.getStartTime()); + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public LocalDate getDate() { + return date; + } + + public LocalTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/entity/Name.java b/src/main/java/roomescape/entity/Name.java new file mode 100644 index 000000000..da6169e6f --- /dev/null +++ b/src/main/java/roomescape/entity/Name.java @@ -0,0 +1,35 @@ +package roomescape.entity; + +import java.util.Objects; + +public class Name { + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 5; + + private final String name; + + public Name(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + validateNonNull(name); + validateLength(name); + } + + private void validateNonNull(String name) { + Objects.requireNonNull(name); + } + + private void validateLength(String name) { + if (MAX_LENGTH < name.length() || name.length() < MIN_LENGTH) { + throw new IllegalArgumentException( + "예약자 이름은 " + MIN_LENGTH + "자 이상, " + MAX_LENGTH + "자 미만이어야 합니다: " + name); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/roomescape/entity/Reservation.java b/src/main/java/roomescape/entity/Reservation.java new file mode 100644 index 000000000..d4a8c6e94 --- /dev/null +++ b/src/main/java/roomescape/entity/Reservation.java @@ -0,0 +1,41 @@ +package roomescape.entity; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class Reservation { + private final Long id; + private final Name name; + private final ReservationTime time; + + public Reservation(Long id, String name, LocalDate reservationDate, LocalTime reservationStartTime) { + this.id = id; + this.name = new Name(name); + this.time = new ReservationTime(LocalDateTime.of(reservationDate, reservationStartTime)); + } + + public Reservation(String name, LocalDate reservationDate, LocalTime reservationStartTime) { + this(null, name, reservationDate, reservationStartTime); + } + + public boolean isConflictWith(Reservation other) { + return time.isConflictWith(other.time); + } + + public long getId() { + return id; + } + + public String getName() { + return name.getName(); + } + + public LocalDate getStartDate() { + return time.getStartDate(); + } + + public LocalTime getStartTime() { + return time.getStartTime(); + } +} diff --git a/src/main/java/roomescape/entity/ReservationTime.java b/src/main/java/roomescape/entity/ReservationTime.java new file mode 100644 index 000000000..c1942dfe2 --- /dev/null +++ b/src/main/java/roomescape/entity/ReservationTime.java @@ -0,0 +1,34 @@ +package roomescape.entity; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Objects; + +public class ReservationTime { + private static final int RESERVATION_DURATION_HOUR = 1; + + private final LocalDateTime reservationStartDateTime; + + public ReservationTime(LocalDateTime reservationStartDateTime) { + Objects.requireNonNull(reservationStartDateTime); + this.reservationStartDateTime = reservationStartDateTime; + } + + public boolean isConflictWith(ReservationTime other) { + return !(endTime().isBefore(other.reservationStartDateTime) || reservationStartDateTime.isAfter( + other.endTime())); + } + + private LocalDateTime endTime() { + return reservationStartDateTime.plusHours(RESERVATION_DURATION_HOUR); + } + + public LocalDate getStartDate() { + return reservationStartDateTime.toLocalDate(); + } + + public LocalTime getStartTime() { + return reservationStartDateTime.toLocalTime(); + } +} diff --git a/src/main/java/roomescape/repository/MemoryReservationRepository.java b/src/main/java/roomescape/repository/MemoryReservationRepository.java new file mode 100644 index 000000000..b70e25582 --- /dev/null +++ b/src/main/java/roomescape/repository/MemoryReservationRepository.java @@ -0,0 +1,53 @@ +package roomescape.repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.stereotype.Repository; +import roomescape.entity.Reservation; + +@Repository +public class MemoryReservationRepository implements ReservationRepository { + private final List reservations = new ArrayList<>(); + private final AtomicLong index = new AtomicLong(1); + + @Override + public List readAll() { + return List.copyOf(reservations); + } + + @Override + public Reservation save(Reservation reservation) { + Reservation saved = new Reservation( + index.getAndIncrement(), + reservation.getName(), + reservation.getStartDate(), + reservation.getStartTime()); + reservations.add(saved); + return saved; + } + + @Override + public Optional findById(long id) { + return reservations.stream() + .filter(reservation -> reservation.getId() == id) + .findAny(); + } + + @Override + public void deleteById(long id) { + Optional found = findById(id); + if (found.isEmpty()) { + throw new NoSuchElementException("해당하는 아이디를 찾을 수 없습니다: " + id); + } + reservations.remove(found.get()); + } + + @Override + public boolean isAnyReservationConflictWith(Reservation reservation) { + return reservations.stream() + .anyMatch(savedReservation -> savedReservation.isConflictWith(reservation)); + } +} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java new file mode 100644 index 000000000..a6731fdca --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -0,0 +1,17 @@ +package roomescape.repository; + +import java.util.List; +import java.util.Optional; +import roomescape.entity.Reservation; + +public interface ReservationRepository { + List readAll(); + + Reservation save(Reservation reservation); + + Optional findById(long id); + + void deleteById(long id); + + boolean isAnyReservationConflictWith(Reservation reservation); +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java new file mode 100644 index 000000000..292b3bb6f --- /dev/null +++ b/src/main/java/roomescape/service/ReservationService.java @@ -0,0 +1,31 @@ +package roomescape.service; + +import java.util.List; +import org.springframework.stereotype.Service; +import roomescape.entity.Reservation; +import roomescape.repository.ReservationRepository; + +@Service +public class ReservationService { + private final ReservationRepository reservationRepository; + + public ReservationService(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } + + public List readAll() { + return reservationRepository.readAll(); + } + + public Reservation saveReservation(Reservation reservation) { + if (reservationRepository.isAnyReservationConflictWith(reservation)) { + throw new IllegalStateException( + "해당 예약 시간에 예약이 이미 존재합니다: " + reservation.getStartDate() + " " + reservation.getStartTime()); + } + return reservationRepository.save(reservation); + } + + public void deleteReservation(long id) { + reservationRepository.deleteById(id); + } +} diff --git a/src/main/resources/templates/admin/index.html b/src/main/resources/templates/admin/index.html index 417102cf1..44716c9f9 100644 --- a/src/main/resources/templates/admin/index.html +++ b/src/main/resources/templates/admin/index.html @@ -1,38 +1,38 @@ - - - 방탈출 어드민 - - - + + + 방탈출 어드민 + + +
-

방탈출 어드민

+

방탈출 어드민

diff --git a/src/main/resources/templates/admin/reservation-legacy.html b/src/main/resources/templates/admin/reservation-legacy.html index 320ef3c70..44ccad54e 100644 --- a/src/main/resources/templates/admin/reservation-legacy.html +++ b/src/main/resources/templates/admin/reservation-legacy.html @@ -1,55 +1,55 @@ - - - 방탈출 어드민 - - - + + + 방탈출 어드민 + + +
-

방탈출 예약 페이지

-
- -
-
- - - - - - - - - - - - -
예약번호예약자날짜시간
+

방탈출 예약 페이지

+
+ +
+
+ + + + + + + + + + + + +
예약번호예약자날짜시간
diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java deleted file mode 100644 index 6f7b19791..000000000 --- a/src/test/java/roomescape/MissionStepTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package roomescape; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStepTest { - - @Test - void 일단계() { - RestAssured.given().log().all() - .when().get("/") - .then().log().all() - .statusCode(200); - } - -} diff --git a/src/test/java/roomescape/controller/AdminViewControllerTest.java b/src/test/java/roomescape/controller/AdminViewControllerTest.java new file mode 100644 index 000000000..5c1db75a0 --- /dev/null +++ b/src/test/java/roomescape/controller/AdminViewControllerTest.java @@ -0,0 +1,28 @@ +package roomescape.controller; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class AdminViewControllerTest { + @DisplayName("어드민 메인 페이지 요청 테스트") + @Test + void adminMainPageTest() { + RestAssured.given().log().all() + .when().get("/admin") + .then().log().all() + .statusCode(200); + } + + @DisplayName("어드민 예약 페이지 요청 테스트") + @Test + void adminReservationPageTest() { + RestAssured.given().log().all() + .when().get("/admin/reservation") + .then().log().all() + .statusCode(200); + } + +} diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java new file mode 100644 index 000000000..edf1a276d --- /dev/null +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -0,0 +1,59 @@ +package roomescape.controller; + +import static org.hamcrest.Matchers.is; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class ReservationControllerTest { + @DisplayName("전체 예약 정보 요청 테스트") + @Test + void reservationsTest() { + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + + @DisplayName("예약 생성, 삭제 테스트") + @Test + void reservationCreationAndDeleteTest() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", "2023-08-05"); + params.put("time", "15:40"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(200) + .body("id", is(1)); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(200); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + +} diff --git a/src/test/java/roomescape/entity/NameTest.java b/src/test/java/roomescape/entity/NameTest.java new file mode 100644 index 000000000..fb9bc7414 --- /dev/null +++ b/src/test/java/roomescape/entity/NameTest.java @@ -0,0 +1,34 @@ +package roomescape.entity; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + @DisplayName("예약자 이름이 null인 경우 생성 시 예외가 발생한다") + @Test + void nullNameCreationTest() { + assertThatThrownBy(() -> new Name(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("예약자 이름의 길이가 범위에 맞지 않는 경우 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", "123456"}) + void invalidLengthNameCreationTest(String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("조건에 맞는 이름을 생성할 경우 예외가 발생하지 않는다") + @ParameterizedTest + @ValueSource(strings = {"1", "12", "123", "1234", "12345"}) + void validNameCreationTest(String name) { + assertThatCode(() -> new Name(name)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/roomescape/entity/ReservationTest.java b/src/test/java/roomescape/entity/ReservationTest.java new file mode 100644 index 000000000..3a9aa3e30 --- /dev/null +++ b/src/test/java/roomescape/entity/ReservationTest.java @@ -0,0 +1,33 @@ +package roomescape.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReservationTest { + @DisplayName("두 예약을 비교, 예약 시간이 겹치는 경우를 알 수 있다") + @Test + void reservationTimeConflictCheckTest() { + LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); + LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 30); + + Reservation reservation1 = new Reservation(1L, "리비", time1.toLocalDate(), time1.toLocalTime()); + Reservation reservation2 = new Reservation(2L, "웨지", time2.toLocalDate(), time2.toLocalTime()); + + assertThat(reservation1.isConflictWith(reservation2)).isTrue(); + } + + @DisplayName("두 예약을 비교, 예약 시간이 겹치지 않는 경우를 알 수 있다") + @Test + void reservationTimeNoConflictCheckTest() { + LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); + LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 31); + + Reservation reservation1 = new Reservation(1L, "리비", time1.toLocalDate(), time1.toLocalTime()); + Reservation reservation2 = new Reservation(2L, "웨지", time2.toLocalDate(), time2.toLocalTime()); + + assertThat(reservation1.isConflictWith(reservation2)).isFalse(); + } +} diff --git a/src/test/java/roomescape/entity/ReservationTimeTest.java b/src/test/java/roomescape/entity/ReservationTimeTest.java new file mode 100644 index 000000000..6948bda9e --- /dev/null +++ b/src/test/java/roomescape/entity/ReservationTimeTest.java @@ -0,0 +1,41 @@ +package roomescape.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReservationTimeTest { + @DisplayName("예약 시간이 null일 경우 생성에 실패한다") + @Test + void reservationTimeCreationTestWithNull() { + assertThatThrownBy(() -> new ReservationTime(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("다른 예약시간과 비교, 예약 시간이 겹치는 경우를 알 수 있다") + @Test + void reservationTimeConflictCheckTest() { + LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); + LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 30); + + ReservationTime reservationTime = new ReservationTime(time1); + ReservationTime conflictTime = new ReservationTime(time2); + + assertThat(reservationTime.isConflictWith(conflictTime)).isTrue(); + } + + @DisplayName("다른 예약시간과 비교, 예약 시간이 겹치지 않는 경우를 알 수 있다") + @Test + void reservationTimeNoConflictCheckTest() { + LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); + LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 31); + + ReservationTime reservationTime = new ReservationTime(time1); + ReservationTime nonConflictTime = new ReservationTime(time2); + + assertThat(reservationTime.isConflictWith(nonConflictTime)).isFalse(); + } +} diff --git a/src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java b/src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java new file mode 100644 index 000000000..6b50b43a8 --- /dev/null +++ b/src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java @@ -0,0 +1,71 @@ +package roomescape.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import roomescape.entity.Reservation; + +class MemoryReservationRepositoryTest { + + private MemoryReservationRepository reservationRepository; + + @BeforeEach + void setUp() { + reservationRepository = new MemoryReservationRepository(); + + Reservation reservation1 = new Reservation(1L, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + Reservation reservation2 = new Reservation(2L, "웨지", LocalDate.of(2024, 4, 20), LocalTime.of(4, 57)); + + reservationRepository.save(reservation1); + reservationRepository.save(reservation2); + } + + @DisplayName("전체 예약을 조회할 수 있다") + @Test + void readAllTest() { + assertThat(reservationRepository.readAll()) + .extracting("id") + .containsExactly(1L, 2L); + } + + @DisplayName("예약 단건을 저장할 수 있다") + @Test + void saveTest() { + Reservation reservation = new Reservation("폭포", LocalDate.of(2024, 5, 20), LocalTime.of(3, 57)); + Reservation saved = reservationRepository.save(reservation); + assertThat(saved.getId()).isEqualTo(3L); + } + + @DisplayName("예약 단건을 조회할 수 있다") + @Test + void findByIdTest() { + assertThat(reservationRepository.findById(1L)).isPresent(); + } + + @DisplayName("예약 단건을 삭제할 수 있다") + @Test + void deleteByIdTest() { + reservationRepository.deleteById(1L); + assertThat(reservationRepository.findById(1L)).isEmpty(); + } + + @DisplayName("예약 단건 삭제 요청 시 존재하지 않는 아이디로 요청하면 예외가 발생한다") + @Test + void deleteByNonExistIdTest() { + assertThatThrownBy(() -> reservationRepository.deleteById(100L)) + .isInstanceOf(NoSuchElementException.class); + } + + @DisplayName("특정 예약이 저장된 예약들과 시간이 겹치는 경우가 있는지 확인할 수 있다") + @Test + void isAnyReservationConflictWithTest() { + Reservation conflictReservation = new Reservation(3L, "폭포", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + assertThat(reservationRepository.isAnyReservationConflictWith(conflictReservation)).isTrue(); + } +} diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java new file mode 100644 index 000000000..dc37400a5 --- /dev/null +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -0,0 +1,48 @@ +package roomescape.service; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.time.LocalDate; +import java.time.LocalTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import roomescape.entity.Reservation; +import roomescape.repository.ReservationRepository; + +class ReservationServiceTest { + @Mock + private ReservationRepository reservationRepository; + + private ReservationService reservationService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + reservationService = new ReservationService(reservationRepository); + } + + @DisplayName("시간이 겹치는 예약이 존재하지 않는 경우 예약에 성공한다") + @Test + void reservationSaveSuccessTest() { + Reservation reservation = new Reservation("리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + when(reservationRepository.isAnyReservationConflictWith(reservation)).thenReturn(false); + + assertThatCode(() -> reservationService.saveReservation(reservation)) + .doesNotThrowAnyException(); + } + + @DisplayName("시간이 겹치는 예약이 존재할 경우 예약에 실패한다") + @Test + void reservationSaveFailByTimeConflictTest() { + Reservation reservation = new Reservation("리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + when(reservationRepository.isAnyReservationConflictWith(reservation)).thenReturn(true); + + assertThatThrownBy(() -> reservationService.saveReservation(reservation)) + .isInstanceOf(IllegalStateException.class); + } +} From 4a8f81e0467d5d1c09e7624ff063134f25de33f1 Mon Sep 17 00:00:00 2001 From: libienz Date: Sun, 21 Apr 2024 20:46:44 +0900 Subject: [PATCH 02/21] =?UTF-8?q?refactor:=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=99=95=EC=9D=B8=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B0=80=EB=8F=85=EC=84=B1=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/entity/ReservationTime.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/roomescape/entity/ReservationTime.java b/src/main/java/roomescape/entity/ReservationTime.java index c1942dfe2..9cc6b84c0 100644 --- a/src/main/java/roomescape/entity/ReservationTime.java +++ b/src/main/java/roomescape/entity/ReservationTime.java @@ -8,27 +8,27 @@ public class ReservationTime { private static final int RESERVATION_DURATION_HOUR = 1; - private final LocalDateTime reservationStartDateTime; + private final LocalDateTime startTime; - public ReservationTime(LocalDateTime reservationStartDateTime) { - Objects.requireNonNull(reservationStartDateTime); - this.reservationStartDateTime = reservationStartDateTime; + public ReservationTime(LocalDateTime startTime) { + Objects.requireNonNull(startTime); + this.startTime = startTime; } public boolean isConflictWith(ReservationTime other) { - return !(endTime().isBefore(other.reservationStartDateTime) || reservationStartDateTime.isAfter( - other.endTime())); + boolean noConflict = endTime().isBefore(other.startTime) || startTime.isAfter(other.endTime()); + return !noConflict; } private LocalDateTime endTime() { - return reservationStartDateTime.plusHours(RESERVATION_DURATION_HOUR); + return startTime.plusHours(RESERVATION_DURATION_HOUR); } public LocalDate getStartDate() { - return reservationStartDateTime.toLocalDate(); + return startTime.toLocalDate(); } public LocalTime getStartTime() { - return reservationStartDateTime.toLocalTime(); + return startTime.toLocalTime(); } } From e899ccb372391bcc0828ac27daec7323fdcf3acd Mon Sep 17 00:00:00 2001 From: libienz Date: Sun, 21 Apr 2024 20:49:20 +0900 Subject: [PATCH 03/21] =?UTF-8?q?refactor:=20NullPointerException=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D=20=EA=B5=AC=EC=B2=B4?= =?UTF-8?q?=ED=99=94=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/entity/Name.java | 6 +++--- src/main/java/roomescape/entity/ReservationTime.java | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/roomescape/entity/Name.java b/src/main/java/roomescape/entity/Name.java index da6169e6f..8ec3f38b6 100644 --- a/src/main/java/roomescape/entity/Name.java +++ b/src/main/java/roomescape/entity/Name.java @@ -1,7 +1,5 @@ package roomescape.entity; -import java.util.Objects; - public class Name { private static final int MIN_LENGTH = 1; private static final int MAX_LENGTH = 5; @@ -19,7 +17,9 @@ private void validate(String name) { } private void validateNonNull(String name) { - Objects.requireNonNull(name); + if (name == null) { + throw new NullPointerException("예약자 이름은 Null이 될 수 없습니다"); + } } private void validateLength(String name) { diff --git a/src/main/java/roomescape/entity/ReservationTime.java b/src/main/java/roomescape/entity/ReservationTime.java index 9cc6b84c0..81ef73dd1 100644 --- a/src/main/java/roomescape/entity/ReservationTime.java +++ b/src/main/java/roomescape/entity/ReservationTime.java @@ -3,7 +3,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.Objects; public class ReservationTime { private static final int RESERVATION_DURATION_HOUR = 1; @@ -11,10 +10,16 @@ public class ReservationTime { private final LocalDateTime startTime; public ReservationTime(LocalDateTime startTime) { - Objects.requireNonNull(startTime); + validateNonNull(startTime); this.startTime = startTime; } + private void validateNonNull(LocalDateTime startTime) { + if (startTime == null) { + throw new NullPointerException("예약 시간은 Null이 될 수 없습니다"); + } + } + public boolean isConflictWith(ReservationTime other) { boolean noConflict = endTime().isBefore(other.startTime) || startTime.isAfter(other.endTime()); return !noConflict; From 67fda4986f96bcd2a5d86f153f2635cc6cf1ff92 Mon Sep 17 00:00:00 2001 From: libienz Date: Sun, 21 Apr 2024 21:58:27 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat:=20Reservation=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/{application.properties => application.yml} | 0 src/main/resources/schema.sql | 8 ++++++++ 2 files changed, 8 insertions(+) rename src/main/resources/{application.properties => application.yml} (100%) create mode 100644 src/main/resources/schema.sql diff --git a/src/main/resources/application.properties b/src/main/resources/application.yml similarity index 100% rename from src/main/resources/application.properties rename to src/main/resources/application.yml diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..8d9ab2754 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date VARCHAR(255) NOT NULL, + time VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); From 6bfe71329f30bd265e8a5718559a06ce43eaa97c Mon Sep 17 00:00:00 2001 From: libienz Date: Sun, 21 Apr 2024 21:58:54 +0900 Subject: [PATCH 05/21] =?UTF-8?q?chore:=20DB=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e69de29bb..ef92e87c8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:database From 0c6e7b29aa3ceb32878004d843a44ad4eeb19309 Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 14:46:01 +0900 Subject: [PATCH 06/21] =?UTF-8?q?refactor:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EB=90=9C=20=EB=B9=88=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...RepositoryTest.java => ReservationRepositoryTest.java} | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) rename src/test/java/roomescape/repository/{MemoryReservationRepositoryTest.java => ReservationRepositoryTest.java} (91%) diff --git a/src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java similarity index 91% rename from src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java rename to src/test/java/roomescape/repository/ReservationRepositoryTest.java index 6b50b43a8..6199542da 100644 --- a/src/test/java/roomescape/repository/MemoryReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -9,11 +9,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import roomescape.entity.Reservation; -class MemoryReservationRepositoryTest { +@SpringBootTest +class ReservationRepositoryTest { - private MemoryReservationRepository reservationRepository; + @Autowired + private ReservationRepository reservationRepository; @BeforeEach void setUp() { From 5e199c7c6a655a66f52c522fa34efab828451fcf Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 15:03:36 +0900 Subject: [PATCH 07/21] =?UTF-8?q?test:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=BB=A4=EB=84=A5=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/repository/H2DatabaseTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/test/java/roomescape/repository/H2DatabaseTest.java diff --git a/src/test/java/roomescape/repository/H2DatabaseTest.java b/src/test/java/roomescape/repository/H2DatabaseTest.java new file mode 100644 index 000000000..72a9b2251 --- /dev/null +++ b/src/test/java/roomescape/repository/H2DatabaseTest.java @@ -0,0 +1,47 @@ +package roomescape.repository; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class H2DatabaseTest { + @Autowired + private JdbcTemplate jdbcTemplate; + + @DisplayName("커넥션을 얻어올 수 있다") + @Test + void connectionTest() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection).isNotNull(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @DisplayName("데이터 베이스의 논리적 구분 이름은 'database'이다") + @Test + void catalogTest() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection.getCatalog()).isEqualTo("DATABASE"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @DisplayName("데이터 베이스에 reservation 테이블이 존재한다") + @Test + void tablesTest() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} From 6b8086ace190ce2ff31eecaddb407a2ffd624187 Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 15:09:28 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20ReservationTime=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20=EC=8B=9C=EA=B0=84=EA=B3=BC=20=EB=81=9D=EB=82=98?= =?UTF-8?q?=EB=8A=94=20=EC=8B=9C=EA=B0=84=20=EB=B0=98=ED=99=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/entity/ReservationTime.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/roomescape/entity/ReservationTime.java b/src/main/java/roomescape/entity/ReservationTime.java index 81ef73dd1..141139395 100644 --- a/src/main/java/roomescape/entity/ReservationTime.java +++ b/src/main/java/roomescape/entity/ReservationTime.java @@ -5,13 +5,13 @@ import java.time.LocalTime; public class ReservationTime { - private static final int RESERVATION_DURATION_HOUR = 1; + public static final int RESERVATION_DURATION_HOUR = 1; - private final LocalDateTime startTime; + private final LocalDateTime start; - public ReservationTime(LocalDateTime startTime) { - validateNonNull(startTime); - this.startTime = startTime; + public ReservationTime(LocalDateTime start) { + validateNonNull(start); + this.start = start; } private void validateNonNull(LocalDateTime startTime) { @@ -21,19 +21,23 @@ private void validateNonNull(LocalDateTime startTime) { } public boolean isConflictWith(ReservationTime other) { - boolean noConflict = endTime().isBefore(other.startTime) || startTime.isAfter(other.endTime()); + boolean noConflict = getEndDateTime().isBefore(other.start) || start.isAfter(other.getEndDateTime()); return !noConflict; } - private LocalDateTime endTime() { - return startTime.plusHours(RESERVATION_DURATION_HOUR); + public LocalDateTime getStartDateTime() { + return start; + } + + public LocalDateTime getEndDateTime() { + return start.plusHours(RESERVATION_DURATION_HOUR); } public LocalDate getStartDate() { - return startTime.toLocalDate(); + return start.toLocalDate(); } - public LocalTime getStartTime() { - return startTime.toLocalTime(); + public LocalTime getStart() { + return start.toLocalTime(); } } From 451c499da903789b956db04003da7e8fe8e63516 Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 15:10:27 +0900 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20Reservation=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=EC=8B=9C=EA=B0=84=EA=B3=BC=20=EB=81=9D?= =?UTF-8?q?=EB=82=98=EB=8A=94=20=EC=8B=9C=EA=B0=84=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/entity/Reservation.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/roomescape/entity/Reservation.java b/src/main/java/roomescape/entity/Reservation.java index d4a8c6e94..a2b918864 100644 --- a/src/main/java/roomescape/entity/Reservation.java +++ b/src/main/java/roomescape/entity/Reservation.java @@ -31,11 +31,19 @@ public String getName() { return name.getName(); } + public LocalDateTime getStartDateTime() { + return time.getStartDateTime(); + } + + public LocalDateTime getEndDateTime() { + return time.getEndDateTime(); + } + public LocalDate getStartDate() { return time.getStartDate(); } public LocalTime getStartTime() { - return time.getStartTime(); + return time.getStart(); } } From b4bfc78c51520939ba8d86db0078c43cc5b13bae Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 17:48:45 +0900 Subject: [PATCH 10/21] =?UTF-8?q?test:=20=EC=8A=A4=ED=94=84=EB=A7=81=20?= =?UTF-8?q?=EB=B6=80=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9E=AC=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/repository/ReservationRepositoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index 6199542da..1494d02fe 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -13,7 +13,7 @@ import org.springframework.boot.test.context.SpringBootTest; import roomescape.entity.Reservation; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class ReservationRepositoryTest { @Autowired From bbef49f595162194883c5b68b9e826dd598bcb13 Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 17:49:43 +0900 Subject: [PATCH 11/21] =?UTF-8?q?feat:=20H2ReservationRepository=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/H2ReservationRepository.java | 93 +++++++++++++++++++ .../MemoryReservationRepository.java | 2 - 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/main/java/roomescape/repository/H2ReservationRepository.java diff --git a/src/main/java/roomescape/repository/H2ReservationRepository.java b/src/main/java/roomescape/repository/H2ReservationRepository.java new file mode 100644 index 000000000..b959a464a --- /dev/null +++ b/src/main/java/roomescape/repository/H2ReservationRepository.java @@ -0,0 +1,93 @@ +package roomescape.repository; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.entity.Reservation; +import roomescape.entity.ReservationTime; + +@Repository +public class H2ReservationRepository implements ReservationRepository { + private final JdbcTemplate jdbcTemplate; + + public H2ReservationRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public List readAll() { + String sql = "select * from reservation"; + return jdbcTemplate.query(sql, reservationRowMapper()); + } + + @Override + public Reservation save(Reservation reservation) { + String sql = "insert into reservation (name, date, time) values(?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, reservation.getName()); + ps.setDate(2, java.sql.Date.valueOf(reservation.getStartDate())); + ps.setTime(3, java.sql.Time.valueOf(reservation.getStartTime())); + return ps; + }, keyHolder); + long savedId = keyHolder.getKey().longValue(); + return new Reservation(savedId, reservation.getName(), reservation.getStartDate(), reservation.getStartTime()); + } + + @Override + public Optional findById(long id) { + String sql = "select * from reservation where id=?"; + try { + return Optional.of(jdbcTemplate.queryForObject(sql, new Object[]{id}, reservationRowMapper())); + } catch (EmptyResultDataAccessException ex) { + return Optional.empty(); + } + } + + @Override + public void deleteById(long id) { + String sql = "delete from reservation where id=?"; + jdbcTemplate.update(sql, id); + } + + @Override + public boolean isAnyReservationConflictWith(Reservation reservation) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String startDateTime = reservation.getStartDateTime().format(formatter); + String endDateTime = reservation.getEndDateTime().format(formatter); + + String sql = "select exists (" + + " select 1 " + + " from reservation " + + " where ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + + " or ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + + ") as exists_overlap;"; + + boolean conflict = jdbcTemplate.queryForObject(sql, Boolean.class, endDateTime, + ReservationTime.RESERVATION_DURATION_HOUR, startDateTime, ReservationTime.RESERVATION_DURATION_HOUR); + return conflict; + } + + private RowMapper reservationRowMapper() { + return (rs, rowNum) -> { + long id = rs.getLong("id"); + String name = rs.getString("name"); + LocalDate date = rs.getDate("date").toLocalDate(); + LocalTime time = rs.getTime("time").toLocalTime(); + + return new Reservation(id, name, date, time); + }; + } +} diff --git a/src/main/java/roomescape/repository/MemoryReservationRepository.java b/src/main/java/roomescape/repository/MemoryReservationRepository.java index b70e25582..2a6354174 100644 --- a/src/main/java/roomescape/repository/MemoryReservationRepository.java +++ b/src/main/java/roomescape/repository/MemoryReservationRepository.java @@ -5,10 +5,8 @@ import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; -import org.springframework.stereotype.Repository; import roomescape.entity.Reservation; -@Repository public class MemoryReservationRepository implements ReservationRepository { private final List reservations = new ArrayList<>(); private final AtomicLong index = new AtomicLong(1); From 38d89f85065d954af5c9ef461f8a950c54909147 Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 17:50:15 +0900 Subject: [PATCH 12/21] =?UTF-8?q?feat:=20ReservationResponse=20Json=20?= =?UTF-8?q?=EC=A7=81=EB=A0=AC=ED=99=94=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/controller/dto/ReservationResponse.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/roomescape/controller/dto/ReservationResponse.java b/src/main/java/roomescape/controller/dto/ReservationResponse.java index d5bd0dc18..f6988bfc3 100644 --- a/src/main/java/roomescape/controller/dto/ReservationResponse.java +++ b/src/main/java/roomescape/controller/dto/ReservationResponse.java @@ -1,5 +1,7 @@ package roomescape.controller.dto; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import java.time.LocalDate; import java.time.LocalTime; import roomescape.entity.Reservation; @@ -10,7 +12,11 @@ public class ReservationResponse { private final LocalDate date; private final LocalTime time; - private ReservationResponse(long id, String name, LocalDate date, LocalTime time) { + @JsonCreator + public ReservationResponse(@JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("date") LocalDate date, + @JsonProperty("time") LocalTime time) { this.id = id; this.name = name; this.date = date; From e87fbbd8f94ff2ec32dfa0ba5a5dec44ffb5092a Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 22 Apr 2024 21:53:33 +0900 Subject: [PATCH 13/21] =?UTF-8?q?chore:=20=EB=B3=B5=EC=88=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EB=A1=9C=EB=93=9C=20=EC=8B=9C=EC=A0=90=20DDL?= =?UTF-8?q?=EC=9D=B4=20=EC=B6=A9=EB=8F=8C=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20SQL=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8d9ab2754..c36eb3485 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,4 +1,4 @@ -CREATE TABLE reservation +CREATE TABLE IF NOT EXISTS reservation ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, From 2f9a19c1377a21e5fb9afa1b72cb3735b02dc8a4 Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 19:52:56 +0900 Subject: [PATCH 14/21] =?UTF-8?q?fix:=20=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=99=80=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20DB=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationControllerTest.java | 6 +- .../repository/ReservationRepositoryTest.java | 59 +++++++++++-------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java index edf1a276d..8ecfd735c 100644 --- a/src/test/java/roomescape/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -30,13 +30,13 @@ void reservationCreationAndDeleteTest() { params.put("date", "2023-08-05"); params.put("time", "15:40"); - RestAssured.given().log().all() + int savedId = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(params) .when().post("/reservations") .then().log().all() .statusCode(200) - .body("id", is(1)); + .extract().path("id"); RestAssured.given().log().all() .when().get("/reservations") @@ -45,7 +45,7 @@ void reservationCreationAndDeleteTest() { .body("size()", is(1)); RestAssured.given().log().all() - .when().delete("/reservations/1") + .when().delete("/reservations/" + savedId) .then().log().all() .statusCode(200); diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index 1494d02fe..6abdf8363 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -1,41 +1,41 @@ package roomescape.repository; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; import java.time.LocalTime; -import java.util.NoSuchElementException; -import org.junit.jupiter.api.BeforeEach; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; import roomescape.entity.Reservation; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@SpringBootTest +@Transactional +@Rollback class ReservationRepositoryTest { @Autowired private ReservationRepository reservationRepository; - @BeforeEach - void setUp() { - reservationRepository = new MemoryReservationRepository(); - - Reservation reservation1 = new Reservation(1L, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); - Reservation reservation2 = new Reservation(2L, "웨지", LocalDate.of(2024, 4, 20), LocalTime.of(4, 57)); + @DisplayName("전체 예약을 조회할 수 있다") + @Test + void readAllTest() { + Reservation reservation1 = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + Reservation reservation2 = new Reservation(null, "웨지", LocalDate.of(2024, 4, 20), LocalTime.of(4, 57)); reservationRepository.save(reservation1); reservationRepository.save(reservation2); - } - @DisplayName("전체 예약을 조회할 수 있다") - @Test - void readAllTest() { + List reservations = reservationRepository.readAll(); + assertThat(reservationRepository.readAll()) - .extracting("id") - .containsExactly(1L, 2L); + .extracting("name") + .containsExactly("리비", "웨지"); } @DisplayName("예약 단건을 저장할 수 있다") @@ -43,33 +43,40 @@ void readAllTest() { void saveTest() { Reservation reservation = new Reservation("폭포", LocalDate.of(2024, 5, 20), LocalTime.of(3, 57)); Reservation saved = reservationRepository.save(reservation); - assertThat(saved.getId()).isEqualTo(3L); + Optional found = reservationRepository.findById(saved.getId()); + + assertThat(found).isPresent(); } @DisplayName("예약 단건을 조회할 수 있다") @Test void findByIdTest() { - assertThat(reservationRepository.findById(1L)).isPresent(); + Reservation reservation = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + Reservation saved = reservationRepository.save(reservation); + + assertThat(reservationRepository.findById(saved.getId())).isPresent(); } @DisplayName("예약 단건을 삭제할 수 있다") @Test void deleteByIdTest() { - reservationRepository.deleteById(1L); - assertThat(reservationRepository.findById(1L)).isEmpty(); - } + Reservation reservation = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); - @DisplayName("예약 단건 삭제 요청 시 존재하지 않는 아이디로 요청하면 예외가 발생한다") - @Test - void deleteByNonExistIdTest() { - assertThatThrownBy(() -> reservationRepository.deleteById(100L)) - .isInstanceOf(NoSuchElementException.class); + Reservation saved = reservationRepository.save(reservation); + reservationRepository.deleteById(saved.getId()); + + assertThat(reservationRepository.findById(1L)).isEmpty(); } @DisplayName("특정 예약이 저장된 예약들과 시간이 겹치는 경우가 있는지 확인할 수 있다") @Test void isAnyReservationConflictWithTest() { + Reservation reservation = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + reservationRepository.save(reservation); Reservation conflictReservation = new Reservation(3L, "폭포", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + assertThat(reservationRepository.isAnyReservationConflictWith(conflictReservation)).isTrue(); } } From cfdf9edc1fedec1a66ee4a0d23739e03477f4bcc Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 19:59:34 +0900 Subject: [PATCH 15/21] =?UTF-8?q?refactor:=20=EC=97=AD=EC=A7=81=EB=A0=AC?= =?UTF-8?q?=ED=99=94=20=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20ResponseDto?= =?UTF-8?q?=EC=97=90=EC=84=9C=20JsonCreator=20=EB=B0=8F=20JsonProperty=20?= =?UTF-8?q?=EC=95=A0=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/controller/dto/ReservationResponse.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/roomescape/controller/dto/ReservationResponse.java b/src/main/java/roomescape/controller/dto/ReservationResponse.java index f6988bfc3..0dd6821de 100644 --- a/src/main/java/roomescape/controller/dto/ReservationResponse.java +++ b/src/main/java/roomescape/controller/dto/ReservationResponse.java @@ -1,7 +1,5 @@ package roomescape.controller.dto; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import java.time.LocalDate; import java.time.LocalTime; import roomescape.entity.Reservation; @@ -12,11 +10,7 @@ public class ReservationResponse { private final LocalDate date; private final LocalTime time; - @JsonCreator - public ReservationResponse(@JsonProperty("id") long id, - @JsonProperty("name") String name, - @JsonProperty("date") LocalDate date, - @JsonProperty("time") LocalTime time) { + public ReservationResponse(long id, String name, LocalDate date, LocalTime time) { this.id = id; this.name = name; this.date = date; From 62c4a3ec41ec019012161dda9ee527162d977dc5 Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 20:01:14 +0900 Subject: [PATCH 16/21] =?UTF-8?q?refactor:=20=EB=B7=B0=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=99=95=EC=9E=A5=EC=9E=90=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/controller/AdminViewController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/roomescape/controller/AdminViewController.java b/src/main/java/roomescape/controller/AdminViewController.java index 8e918f1ef..7efcc5654 100644 --- a/src/main/java/roomescape/controller/AdminViewController.java +++ b/src/main/java/roomescape/controller/AdminViewController.java @@ -9,7 +9,7 @@ public class AdminViewController { @GetMapping() public String showAdminMainPage() { - return "admin/index.html"; + return "admin/index"; } @GetMapping("/reservation") From b69655c0bcc2e7f908862f08714c1cfcb92b1174 Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 20:06:50 +0900 Subject: [PATCH 17/21] =?UTF-8?q?test:=20=EC=98=88=EC=95=BD=20=EC=A7=80?= =?UTF-8?q?=EC=86=8D=20=EC=8B=9C=EA=B0=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/entity/ReservationTimeTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/roomescape/entity/ReservationTimeTest.java b/src/test/java/roomescape/entity/ReservationTimeTest.java index 6948bda9e..0fb81dadf 100644 --- a/src/test/java/roomescape/entity/ReservationTimeTest.java +++ b/src/test/java/roomescape/entity/ReservationTimeTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static roomescape.entity.ReservationTime.RESERVATION_DURATION_HOUR; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; @@ -15,6 +16,15 @@ void reservationTimeCreationTestWithNull() { .isInstanceOf(NullPointerException.class); } + @DisplayName("예약이 끝나는 시간은 시작 시간으로부터 지정된 기간 뒤 까지이다") + @Test + void reservationDurationTest() { + LocalDateTime startTime = LocalDateTime.of(2024, 4, 20, 12, 30); + ReservationTime reservationTime = new ReservationTime(startTime); + + assertThat(reservationTime.getEndDateTime()).isEqualTo(startTime.plusHours(RESERVATION_DURATION_HOUR)); + } + @DisplayName("다른 예약시간과 비교, 예약 시간이 겹치는 경우를 알 수 있다") @Test void reservationTimeConflictCheckTest() { From addeb22945acb8f99826db3e7f2e0c6e5a6306a6 Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 20:09:16 +0900 Subject: [PATCH 18/21] =?UTF-8?q?refactor:=20import=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20=EA=B0=80=EB=8F=85=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/repository/H2ReservationRepository.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/roomescape/repository/H2ReservationRepository.java b/src/main/java/roomescape/repository/H2ReservationRepository.java index b959a464a..860f53018 100644 --- a/src/main/java/roomescape/repository/H2ReservationRepository.java +++ b/src/main/java/roomescape/repository/H2ReservationRepository.java @@ -1,7 +1,9 @@ package roomescape.repository; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.Statement; +import java.sql.Time; import java.time.LocalDate; import java.time.LocalTime; import java.time.format.DateTimeFormatter; @@ -38,8 +40,8 @@ public Reservation save(Reservation reservation) { jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, reservation.getName()); - ps.setDate(2, java.sql.Date.valueOf(reservation.getStartDate())); - ps.setTime(3, java.sql.Time.valueOf(reservation.getStartTime())); + ps.setDate(2, Date.valueOf(reservation.getStartDate())); + ps.setTime(3, Time.valueOf(reservation.getStartTime())); return ps; }, keyHolder); long savedId = keyHolder.getKey().longValue(); From 25f07751712f0c3fa5ec45fd9cbc65a7f7af2496 Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 20:16:14 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor:=20=EC=BF=BC=EB=A6=AC=EB=A5=BC?= =?UTF-8?q?=20=ED=86=B5=ED=95=B4=20=EC=98=88=EC=95=BD=20=EC=B6=A9=EB=8F=8C?= =?UTF-8?q?=EC=9D=84=20=ED=95=B4=EA=B2=B0=ED=95=98=EA=B2=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=A8=20-=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/entity/Reservation.java | 4 -- .../roomescape/entity/ReservationTime.java | 5 -- .../MemoryReservationRepository.java | 51 ------------------- .../roomescape/entity/ReservationTest.java | 26 +++------- .../entity/ReservationTimeTest.java | 24 --------- 5 files changed, 6 insertions(+), 104 deletions(-) delete mode 100644 src/main/java/roomescape/repository/MemoryReservationRepository.java diff --git a/src/main/java/roomescape/entity/Reservation.java b/src/main/java/roomescape/entity/Reservation.java index a2b918864..3735a116d 100644 --- a/src/main/java/roomescape/entity/Reservation.java +++ b/src/main/java/roomescape/entity/Reservation.java @@ -19,10 +19,6 @@ public Reservation(String name, LocalDate reservationDate, LocalTime reservation this(null, name, reservationDate, reservationStartTime); } - public boolean isConflictWith(Reservation other) { - return time.isConflictWith(other.time); - } - public long getId() { return id; } diff --git a/src/main/java/roomescape/entity/ReservationTime.java b/src/main/java/roomescape/entity/ReservationTime.java index 141139395..76a3e49b2 100644 --- a/src/main/java/roomescape/entity/ReservationTime.java +++ b/src/main/java/roomescape/entity/ReservationTime.java @@ -20,11 +20,6 @@ private void validateNonNull(LocalDateTime startTime) { } } - public boolean isConflictWith(ReservationTime other) { - boolean noConflict = getEndDateTime().isBefore(other.start) || start.isAfter(other.getEndDateTime()); - return !noConflict; - } - public LocalDateTime getStartDateTime() { return start; } diff --git a/src/main/java/roomescape/repository/MemoryReservationRepository.java b/src/main/java/roomescape/repository/MemoryReservationRepository.java deleted file mode 100644 index 2a6354174..000000000 --- a/src/main/java/roomescape/repository/MemoryReservationRepository.java +++ /dev/null @@ -1,51 +0,0 @@ -package roomescape.repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import roomescape.entity.Reservation; - -public class MemoryReservationRepository implements ReservationRepository { - private final List reservations = new ArrayList<>(); - private final AtomicLong index = new AtomicLong(1); - - @Override - public List readAll() { - return List.copyOf(reservations); - } - - @Override - public Reservation save(Reservation reservation) { - Reservation saved = new Reservation( - index.getAndIncrement(), - reservation.getName(), - reservation.getStartDate(), - reservation.getStartTime()); - reservations.add(saved); - return saved; - } - - @Override - public Optional findById(long id) { - return reservations.stream() - .filter(reservation -> reservation.getId() == id) - .findAny(); - } - - @Override - public void deleteById(long id) { - Optional found = findById(id); - if (found.isEmpty()) { - throw new NoSuchElementException("해당하는 아이디를 찾을 수 없습니다: " + id); - } - reservations.remove(found.get()); - } - - @Override - public boolean isAnyReservationConflictWith(Reservation reservation) { - return reservations.stream() - .anyMatch(savedReservation -> savedReservation.isConflictWith(reservation)); - } -} diff --git a/src/test/java/roomescape/entity/ReservationTest.java b/src/test/java/roomescape/entity/ReservationTest.java index 3a9aa3e30..a41f3c7d1 100644 --- a/src/test/java/roomescape/entity/ReservationTest.java +++ b/src/test/java/roomescape/entity/ReservationTest.java @@ -1,33 +1,19 @@ package roomescape.entity; import static org.assertj.core.api.Assertions.assertThat; +import static roomescape.entity.ReservationTime.RESERVATION_DURATION_HOUR; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class ReservationTest { - @DisplayName("두 예약을 비교, 예약 시간이 겹치는 경우를 알 수 있다") + @DisplayName("예약이 끝나는 시간을 계산할 수 있다") @Test - void reservationTimeConflictCheckTest() { - LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); - LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 30); + void calculateReservationEndTimeTest() { + LocalDateTime startTime = LocalDateTime.of(2024, 4, 20, 12, 30); + Reservation reservation = new Reservation("리비", startTime.toLocalDate(), startTime.toLocalTime()); - Reservation reservation1 = new Reservation(1L, "리비", time1.toLocalDate(), time1.toLocalTime()); - Reservation reservation2 = new Reservation(2L, "웨지", time2.toLocalDate(), time2.toLocalTime()); - - assertThat(reservation1.isConflictWith(reservation2)).isTrue(); - } - - @DisplayName("두 예약을 비교, 예약 시간이 겹치지 않는 경우를 알 수 있다") - @Test - void reservationTimeNoConflictCheckTest() { - LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); - LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 31); - - Reservation reservation1 = new Reservation(1L, "리비", time1.toLocalDate(), time1.toLocalTime()); - Reservation reservation2 = new Reservation(2L, "웨지", time2.toLocalDate(), time2.toLocalTime()); - - assertThat(reservation1.isConflictWith(reservation2)).isFalse(); + assertThat(reservation.getEndDateTime()).isEqualTo(startTime.plusHours(RESERVATION_DURATION_HOUR)); } } diff --git a/src/test/java/roomescape/entity/ReservationTimeTest.java b/src/test/java/roomescape/entity/ReservationTimeTest.java index 0fb81dadf..d8e0d904f 100644 --- a/src/test/java/roomescape/entity/ReservationTimeTest.java +++ b/src/test/java/roomescape/entity/ReservationTimeTest.java @@ -24,28 +24,4 @@ void reservationDurationTest() { assertThat(reservationTime.getEndDateTime()).isEqualTo(startTime.plusHours(RESERVATION_DURATION_HOUR)); } - - @DisplayName("다른 예약시간과 비교, 예약 시간이 겹치는 경우를 알 수 있다") - @Test - void reservationTimeConflictCheckTest() { - LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); - LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 30); - - ReservationTime reservationTime = new ReservationTime(time1); - ReservationTime conflictTime = new ReservationTime(time2); - - assertThat(reservationTime.isConflictWith(conflictTime)).isTrue(); - } - - @DisplayName("다른 예약시간과 비교, 예약 시간이 겹치지 않는 경우를 알 수 있다") - @Test - void reservationTimeNoConflictCheckTest() { - LocalDateTime time1 = LocalDateTime.of(2024, 4, 20, 12, 30); - LocalDateTime time2 = LocalDateTime.of(2024, 4, 20, 13, 31); - - ReservationTime reservationTime = new ReservationTime(time1); - ReservationTime nonConflictTime = new ReservationTime(time2); - - assertThat(reservationTime.isConflictWith(nonConflictTime)).isFalse(); - } } From 545ef8cf2898122482a06a6b09894713bfbfe7dc Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 20:18:35 +0900 Subject: [PATCH 20/21] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=99=95=EC=9E=A5=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EA=B5=AC=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/H2ReservationRepository.java | 95 ------------------- .../repository/ReservationRepository.java | 85 +++++++++++++++-- 2 files changed, 79 insertions(+), 101 deletions(-) delete mode 100644 src/main/java/roomescape/repository/H2ReservationRepository.java diff --git a/src/main/java/roomescape/repository/H2ReservationRepository.java b/src/main/java/roomescape/repository/H2ReservationRepository.java deleted file mode 100644 index 860f53018..000000000 --- a/src/main/java/roomescape/repository/H2ReservationRepository.java +++ /dev/null @@ -1,95 +0,0 @@ -package roomescape.repository; - -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.sql.Time; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Optional; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.entity.Reservation; -import roomescape.entity.ReservationTime; - -@Repository -public class H2ReservationRepository implements ReservationRepository { - private final JdbcTemplate jdbcTemplate; - - public H2ReservationRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - @Override - public List readAll() { - String sql = "select * from reservation"; - return jdbcTemplate.query(sql, reservationRowMapper()); - } - - @Override - public Reservation save(Reservation reservation) { - String sql = "insert into reservation (name, date, time) values(?, ?, ?)"; - KeyHolder keyHolder = new GeneratedKeyHolder(); - - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, reservation.getName()); - ps.setDate(2, Date.valueOf(reservation.getStartDate())); - ps.setTime(3, Time.valueOf(reservation.getStartTime())); - return ps; - }, keyHolder); - long savedId = keyHolder.getKey().longValue(); - return new Reservation(savedId, reservation.getName(), reservation.getStartDate(), reservation.getStartTime()); - } - - @Override - public Optional findById(long id) { - String sql = "select * from reservation where id=?"; - try { - return Optional.of(jdbcTemplate.queryForObject(sql, new Object[]{id}, reservationRowMapper())); - } catch (EmptyResultDataAccessException ex) { - return Optional.empty(); - } - } - - @Override - public void deleteById(long id) { - String sql = "delete from reservation where id=?"; - jdbcTemplate.update(sql, id); - } - - @Override - public boolean isAnyReservationConflictWith(Reservation reservation) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - String startDateTime = reservation.getStartDateTime().format(formatter); - String endDateTime = reservation.getEndDateTime().format(formatter); - - String sql = "select exists (" + - " select 1 " + - " from reservation " + - " where ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + - " or ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + - ") as exists_overlap;"; - - boolean conflict = jdbcTemplate.queryForObject(sql, Boolean.class, endDateTime, - ReservationTime.RESERVATION_DURATION_HOUR, startDateTime, ReservationTime.RESERVATION_DURATION_HOUR); - return conflict; - } - - private RowMapper reservationRowMapper() { - return (rs, rowNum) -> { - long id = rs.getLong("id"); - String name = rs.getString("name"); - LocalDate date = rs.getDate("date").toLocalDate(); - LocalTime time = rs.getTime("time").toLocalTime(); - - return new Reservation(id, name, date, time); - }; - } -} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index a6731fdca..43f1c669e 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -1,17 +1,90 @@ package roomescape.repository; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.sql.Time; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; import roomescape.entity.Reservation; +import roomescape.entity.ReservationTime; -public interface ReservationRepository { - List readAll(); +@Repository +public class ReservationRepository { + private final JdbcTemplate jdbcTemplate; - Reservation save(Reservation reservation); + public ReservationRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } - Optional findById(long id); + public List readAll() { + String sql = "select * from reservation"; + return jdbcTemplate.query(sql, reservationRowMapper()); + } - void deleteById(long id); + public Reservation save(Reservation reservation) { + String sql = "insert into reservation (name, date, time) values(?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); - boolean isAnyReservationConflictWith(Reservation reservation); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, reservation.getName()); + ps.setDate(2, Date.valueOf(reservation.getStartDate())); + ps.setTime(3, Time.valueOf(reservation.getStartTime())); + return ps; + }, keyHolder); + long savedId = keyHolder.getKey().longValue(); + return new Reservation(savedId, reservation.getName(), reservation.getStartDate(), reservation.getStartTime()); + } + + public Optional findById(long id) { + String sql = "select * from reservation where id=?"; + try { + return Optional.of(jdbcTemplate.queryForObject(sql, new Object[]{id}, reservationRowMapper())); + } catch (EmptyResultDataAccessException ex) { + return Optional.empty(); + } + } + + public void deleteById(long id) { + String sql = "delete from reservation where id=?"; + jdbcTemplate.update(sql, id); + } + + public boolean isAnyReservationConflictWith(Reservation reservation) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String startDateTime = reservation.getStartDateTime().format(formatter); + String endDateTime = reservation.getEndDateTime().format(formatter); + + String sql = "select exists (" + + " select 1 " + + " from reservation " + + " where ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + + " or ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + + ") as exists_overlap;"; + + boolean conflict = jdbcTemplate.queryForObject(sql, Boolean.class, endDateTime, + ReservationTime.RESERVATION_DURATION_HOUR, startDateTime, ReservationTime.RESERVATION_DURATION_HOUR); + return conflict; + } + + private RowMapper reservationRowMapper() { + return (rs, rowNum) -> { + long id = rs.getLong("id"); + String name = rs.getString("name"); + LocalDate date = rs.getDate("date").toLocalDate(); + LocalTime time = rs.getTime("time").toLocalTime(); + + return new Reservation(id, name, date, time); + }; + } } From 01299a503d7a48810d2f24a7057ba1a1797ae270 Mon Sep 17 00:00:00 2001 From: libienz Date: Tue, 23 Apr 2024 20:25:02 +0900 Subject: [PATCH 21/21] =?UTF-8?q?test:=20Service=20=EA=B3=84=EC=B8=B5=20?= =?UTF-8?q?=EC=8A=AC=EB=9D=BC=EC=9D=B4=EC=8A=A4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B8=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationServiceTest.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index dc37400a5..cda92fa7d 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -2,35 +2,28 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; import java.time.LocalDate; import java.time.LocalTime; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; import roomescape.entity.Reservation; -import roomescape.repository.ReservationRepository; +@SpringBootTest +@Transactional +@Rollback class ReservationServiceTest { - @Mock - private ReservationRepository reservationRepository; - + @Autowired private ReservationService reservationService; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - reservationService = new ReservationService(reservationRepository); - } - @DisplayName("시간이 겹치는 예약이 존재하지 않는 경우 예약에 성공한다") @Test void reservationSaveSuccessTest() { Reservation reservation = new Reservation("리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); - when(reservationRepository.isAnyReservationConflictWith(reservation)).thenReturn(false); assertThatCode(() -> reservationService.saveReservation(reservation)) .doesNotThrowAnyException(); @@ -40,9 +33,10 @@ void reservationSaveSuccessTest() { @Test void reservationSaveFailByTimeConflictTest() { Reservation reservation = new Reservation("리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); - when(reservationRepository.isAnyReservationConflictWith(reservation)).thenReturn(true); + Reservation conflictReservation = new Reservation("웨지", LocalDate.of(2024, 4, 20), LocalTime.of(3, 30)); + reservationService.saveReservation(reservation); - assertThatThrownBy(() -> reservationService.saveReservation(reservation)) + assertThatThrownBy(() -> reservationService.saveReservation(conflictReservation)) .isInstanceOf(IllegalStateException.class); } }