From dca0905f2a95c6bbd5329a1a6747409b1f944780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Tue, 27 May 2025 21:59:46 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat(step1)=20:=201=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=ED=99=88=20=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../java/roomescape/controller/HomeController.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/main/java/roomescape/controller/HomeController.java diff --git a/build.gradle b/build.gradle index 57267157c..43db8c2b0 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,9 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' } diff --git a/src/main/java/roomescape/controller/HomeController.java b/src/main/java/roomescape/controller/HomeController.java new file mode 100644 index 000000000..4a3da7ea2 --- /dev/null +++ b/src/main/java/roomescape/controller/HomeController.java @@ -0,0 +1,14 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + @GetMapping("/") + public String home() { + return "home"; + } +} + From 96ce9ef0dd548cb81952495cf7409f3c45840de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Tue, 27 May 2025 22:40:15 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat(step2)=20:=202=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../roomescape/controller/HomeController.java | 14 ------ .../controller/RoomescapeController.java | 44 +++++++++++++++++++ .../java/roomescape/domain/Reservation.java | 34 ++++++++++++++ .../roomescape/dto/ReservationResponse.java | 22 ++++++++++ src/test/java/roomescape/MissionStepTest.java | 16 +++++++ 6 files changed, 117 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/roomescape/controller/HomeController.java create mode 100644 src/main/java/roomescape/controller/RoomescapeController.java create mode 100644 src/main/java/roomescape/domain/Reservation.java create mode 100644 src/main/java/roomescape/dto/ReservationResponse.java diff --git a/build.gradle b/build.gradle index 43db8c2b0..fb96eaae1 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.hamcrest:hamcrest:2.2' } test { diff --git a/src/main/java/roomescape/controller/HomeController.java b/src/main/java/roomescape/controller/HomeController.java deleted file mode 100644 index 4a3da7ea2..000000000 --- a/src/main/java/roomescape/controller/HomeController.java +++ /dev/null @@ -1,14 +0,0 @@ -package roomescape.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class HomeController { - - @GetMapping("/") - public String home() { - return "home"; - } -} - diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java new file mode 100644 index 000000000..c3c17d5dd --- /dev/null +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -0,0 +1,44 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import roomescape.domain.Reservation; +import roomescape.dto.ReservationResponse; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Controller +public class RoomescapeController { + + private final List reservations = new ArrayList<>(); + + public RoomescapeController() { + reservations.add(new Reservation(1L, "오찌", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); + reservations.add(new Reservation(2L, "장순", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); + reservations.add(new Reservation(3L, "희정", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); + reservations.add(new Reservation(4L, "예진", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); + } + + @GetMapping("/") + public String home() { + return "home"; + } + + @GetMapping("/reservation") + public String reservationPage() { + return "reservation"; + } + + @GetMapping("/reservations") + @ResponseBody + public List reservations() { + return reservations.stream() + .map(ReservationResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java new file mode 100644 index 000000000..1d3ce419a --- /dev/null +++ b/src/main/java/roomescape/domain/Reservation.java @@ -0,0 +1,34 @@ +package roomescape.domain; + +import java.time.LocalDate; +import java.time.LocalTime; + +public class Reservation { + private final Long id; + private final String name; + private final LocalDate date; + private final LocalTime time; + + public Reservation(Long id, String name, LocalDate date, LocalTime time) { + this.id = id; + this.name = name; + this.date = date; + this.time = time; + } + + 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/dto/ReservationResponse.java b/src/main/java/roomescape/dto/ReservationResponse.java new file mode 100644 index 000000000..4fa8e071e --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationResponse.java @@ -0,0 +1,22 @@ +package roomescape.dto; + +import roomescape.domain.Reservation; + +import java.time.LocalDate; +import java.time.LocalTime; + +public record ReservationResponse( + Long id, + String name, + LocalDate date, + LocalTime time +) { + public static ReservationResponse from(Reservation reservation) { + return new ReservationResponse( + reservation.getId(), + reservation.getName(), + reservation.getDate(), + reservation.getTime() + ); + } +} diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index cf4efbe91..92e16a7ab 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -5,6 +5,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import static org.hamcrest.Matchers.is; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { @@ -16,4 +18,18 @@ public class MissionStepTest { .then().log().all() .statusCode(200); } + + @Test + void 이단계() { + RestAssured.given().log().all() + .when().get("/reservation") + .then().log().all() + .statusCode(200); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(4)); + } } From 66299699884af0488a7312fb731994d3e5d6e9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Tue, 27 May 2025 23:34:04 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat(step3)=20:=203=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20=EC=B6=94=EA=B0=80=20/=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomescapeController.java | 49 +++++++++++++++++-- .../roomescape/dto/ReservationRequest.java | 18 +++++++ src/test/java/roomescape/MissionStepTest.java | 40 ++++++++++++++- 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 src/main/java/roomescape/dto/ReservationRequest.java diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java index c3c17d5dd..c0217340a 100644 --- a/src/main/java/roomescape/controller/RoomescapeController.java +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -1,27 +1,42 @@ package roomescape.controller; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +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.ResponseBody; import roomescape.domain.Reservation; +import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; +import java.net.URI; import java.time.LocalDate; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @Controller public class RoomescapeController { private final List reservations = new ArrayList<>(); + private AtomicLong index; public RoomescapeController() { - reservations.add(new Reservation(1L, "오찌", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); - reservations.add(new Reservation(2L, "장순", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); - reservations.add(new Reservation(3L, "희정", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); - reservations.add(new Reservation(4L, "예진", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); +// reservations.add(new Reservation(1L, "오찌", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); +// reservations.add(new Reservation(2L, "장순", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); +// reservations.add(new Reservation(3L, "희정", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); +// reservations.add(new Reservation(4L, "예진", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); + + long maxId = reservations.stream() + .mapToLong(Reservation::getId) + .max() + .orElse(0L); + this.index = new AtomicLong(maxId); } @GetMapping("/") @@ -41,4 +56,30 @@ public List reservations() { .map(ReservationResponse::from) .collect(Collectors.toList()); } + + @PostMapping("/reservations") + public ResponseEntity addReservation( + @RequestBody ReservationRequest reservationRequest + ) { + Long newId = index.incrementAndGet(); + Reservation newReservation = new Reservation( + newId, + reservationRequest.name(), + reservationRequest.date(), + reservationRequest.time()); + + reservations.add(newReservation); + + return ResponseEntity + .created(URI.create("/reservations/" + newId)) + .body(ReservationResponse.from(newReservation)); + } + + @DeleteMapping("/reservations/{id}") + public ResponseEntity cancelReservation( + @PathVariable Long id + ) { + reservations.removeIf(reservation -> reservation.getId().equals(id)); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/roomescape/dto/ReservationRequest.java b/src/main/java/roomescape/dto/ReservationRequest.java new file mode 100644 index 000000000..1f31439f7 --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationRequest.java @@ -0,0 +1,18 @@ +package roomescape.dto; + +import java.time.LocalDate; +import java.time.LocalTime; + +public record ReservationRequest( + LocalDate date, + String name, + LocalTime time +) { + public static ReservationRequest of(String date, String name, String time) { + return new ReservationRequest( + LocalDate.parse(date), + name, + LocalTime.parse(time) + ); + } +} diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 92e16a7ab..4de9ed1d6 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -1,10 +1,14 @@ package roomescape; import io.restassured.RestAssured; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import java.util.HashMap; +import java.util.Map; + import static org.hamcrest.Matchers.is; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @@ -30,6 +34,40 @@ public class MissionStepTest { .when().get("/reservations") .then().log().all() .statusCode(200) - .body("size()", is(4)); + .body("size()", is(0)); + } + + @Test + void 삼단계() { + Map params = new HashMap<>(); + params.put("name", "오찌"); + params.put("date", "2025-06-02"); + params.put("time", "17:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .header("Location", "/reservations/1") + .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(204); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); } } From 5b5b0971d990a181d2a55f1af566aa281ca9ea0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Thu, 29 May 2025 13:39:43 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat(step4)=20:=204=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomescapeController.java | 57 +++++-------------- .../domain/InMemoryReservationRepository.java | 45 +++++++++++++++ .../java/roomescape/domain/Reservation.java | 36 +++++++++++- .../roomescape/dto/ReservationRequest.java | 30 ++++++---- .../InvalidReservationRequestException.java | 7 +++ .../ReservationExceptionHandler.java | 19 +++++++ .../ReservationNotFoundException.java | 7 +++ .../service/ReservationService.java | 35 ++++++++++++ src/test/java/roomescape/MissionStepTest.java | 22 +++++++ 9 files changed, 202 insertions(+), 56 deletions(-) create mode 100644 src/main/java/roomescape/domain/InMemoryReservationRepository.java create mode 100644 src/main/java/roomescape/exception/InvalidReservationRequestException.java create mode 100644 src/main/java/roomescape/exception/ReservationExceptionHandler.java create mode 100644 src/main/java/roomescape/exception/ReservationNotFoundException.java create mode 100644 src/main/java/roomescape/service/ReservationService.java diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java index c0217340a..f3e46ae08 100644 --- a/src/main/java/roomescape/controller/RoomescapeController.java +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -1,42 +1,27 @@ package roomescape.controller; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; 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.ResponseBody; -import roomescape.domain.Reservation; +import org.springframework.web.bind.annotation.RestController; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; +import roomescape.service.ReservationService; import java.net.URI; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -@Controller +@RestController public class RoomescapeController { - private final List reservations = new ArrayList<>(); - private AtomicLong index; + private final ReservationService reservationService; - public RoomescapeController() { -// reservations.add(new Reservation(1L, "오찌", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); -// reservations.add(new Reservation(2L, "장순", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); -// reservations.add(new Reservation(3L, "희정", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); -// reservations.add(new Reservation(4L, "예진", LocalDate.of(2025, 6, 2), LocalTime.of(17, 0))); - - long maxId = reservations.stream() - .mapToLong(Reservation::getId) - .max() - .orElse(0L); - this.index = new AtomicLong(maxId); + public RoomescapeController(ReservationService reservationService) { + this.reservationService = reservationService; } @GetMapping("/") @@ -51,35 +36,19 @@ public String reservationPage() { @GetMapping("/reservations") @ResponseBody - public List reservations() { - return reservations.stream() - .map(ReservationResponse::from) - .collect(Collectors.toList()); + public List getReservations() { + return reservationService.findAll(); } @PostMapping("/reservations") - public ResponseEntity addReservation( - @RequestBody ReservationRequest reservationRequest - ) { - Long newId = index.incrementAndGet(); - Reservation newReservation = new Reservation( - newId, - reservationRequest.name(), - reservationRequest.date(), - reservationRequest.time()); - - reservations.add(newReservation); - - return ResponseEntity - .created(URI.create("/reservations/" + newId)) - .body(ReservationResponse.from(newReservation)); + public ResponseEntity createReservation(@RequestBody ReservationRequest request) { + ReservationResponse reservation = reservationService.create(request); + return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); } @DeleteMapping("/reservations/{id}") - public ResponseEntity cancelReservation( - @PathVariable Long id - ) { - reservations.removeIf(reservation -> reservation.getId().equals(id)); + public ResponseEntity deleteReservation(@PathVariable Long id) { + reservationService.delete(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/domain/InMemoryReservationRepository.java b/src/main/java/roomescape/domain/InMemoryReservationRepository.java new file mode 100644 index 000000000..180b29fd9 --- /dev/null +++ b/src/main/java/roomescape/domain/InMemoryReservationRepository.java @@ -0,0 +1,45 @@ +package roomescape.domain; + +import org.springframework.stereotype.Repository; +import roomescape.exception.ReservationNotFoundException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +@Repository +public class InMemoryReservationRepository { + private final List reservations = new ArrayList<>(); + private final AtomicLong sequence = new AtomicLong(0L); + + public Reservation save(Reservation reservation) { + Reservation storedReservation = Reservation.of( + sequence.incrementAndGet(), + reservation.getName(), + reservation.getDate(), + reservation.getTime() + ); + reservations.add(storedReservation); + return storedReservation; + } + + public void deleteById(Long id) { + Reservation reservation = findById(id) + .orElseThrow(() -> new ReservationNotFoundException("[ERROR] 예약을 찾을 수 없습니다.")); + reservations.remove(reservation); + } + + public List findAll() { + return Collections.unmodifiableList(reservations); + } + + public Optional findById(Long id) { + return reservations.stream() + .filter(reservation -> reservation.getId().equals(id)) + .findFirst(); + } +} + + diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java index 1d3ce419a..fc3daacd5 100644 --- a/src/main/java/roomescape/domain/Reservation.java +++ b/src/main/java/roomescape/domain/Reservation.java @@ -9,13 +9,22 @@ public class Reservation { private final LocalDate date; private final LocalTime time; - public Reservation(Long id, String name, LocalDate date, LocalTime time) { + private Reservation(Long id, String name, LocalDate date, LocalTime time) { + validate(name, date, time); this.id = id; this.name = name; this.date = date; this.time = time; } + public static Reservation of(Long id, String name, LocalDate date, LocalTime time) { + return new Reservation(id, name, date, time); + } + + public static Reservation of(String name, LocalDate date, LocalTime time) { + return new Reservation(null, name, date, time); + } + public Long getId() { return id; } @@ -31,4 +40,29 @@ public LocalDate getDate() { public LocalTime getTime() { return time; } + + private void validate(String name, LocalDate date, LocalTime time) { + validateName(name); + validateDate(date); + validateTime(time); + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("[ERROR] 이름은 필수 입력 항목입니다."); + } + } + + private void validateDate(LocalDate date) { + if (date == null) { + throw new IllegalArgumentException("[ERROR] 날짜는 필수 입력 항목입니다."); + } + } + + private void validateTime(LocalTime time) { + if (time == null) { + throw new IllegalArgumentException("[ERROR] 시간은 필수 입력 항목입니다."); + } + } } + diff --git a/src/main/java/roomescape/dto/ReservationRequest.java b/src/main/java/roomescape/dto/ReservationRequest.java index 1f31439f7..bbc17099c 100644 --- a/src/main/java/roomescape/dto/ReservationRequest.java +++ b/src/main/java/roomescape/dto/ReservationRequest.java @@ -1,18 +1,26 @@ package roomescape.dto; +import roomescape.exception.InvalidReservationRequestException; + import java.time.LocalDate; import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +public record ReservationRequest(String date, String name, String time) { + + public LocalDate parseDate() { + try { + return LocalDate.parse(date); + } catch (DateTimeParseException e) { + throw new InvalidReservationRequestException("[ERROR] 날짜 형식이 잘못되었습니다."); + } + } -public record ReservationRequest( - LocalDate date, - String name, - LocalTime time -) { - public static ReservationRequest of(String date, String name, String time) { - return new ReservationRequest( - LocalDate.parse(date), - name, - LocalTime.parse(time) - ); + public LocalTime parseTime() { + try { + return LocalTime.parse(time); + } catch (DateTimeParseException e) { + throw new InvalidReservationRequestException("[ERROR] 시간 형식이 잘못되었습니다."); + } } } diff --git a/src/main/java/roomescape/exception/InvalidReservationRequestException.java b/src/main/java/roomescape/exception/InvalidReservationRequestException.java new file mode 100644 index 000000000..8bccf3a54 --- /dev/null +++ b/src/main/java/roomescape/exception/InvalidReservationRequestException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class InvalidReservationRequestException extends RuntimeException { + public InvalidReservationRequestException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/exception/ReservationExceptionHandler.java b/src/main/java/roomescape/exception/ReservationExceptionHandler.java new file mode 100644 index 000000000..5939f7f29 --- /dev/null +++ b/src/main/java/roomescape/exception/ReservationExceptionHandler.java @@ -0,0 +1,19 @@ +package roomescape.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ReservationExceptionHandler { + + @ExceptionHandler(InvalidReservationRequestException.class) + public ResponseEntity handleInvalidReservation(InvalidReservationRequestException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + + @ExceptionHandler(ReservationNotFoundException.class) + public ResponseEntity handleNotFound(ReservationNotFoundException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } +} diff --git a/src/main/java/roomescape/exception/ReservationNotFoundException.java b/src/main/java/roomescape/exception/ReservationNotFoundException.java new file mode 100644 index 000000000..a101dd0f7 --- /dev/null +++ b/src/main/java/roomescape/exception/ReservationNotFoundException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class ReservationNotFoundException extends RuntimeException { + public ReservationNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java new file mode 100644 index 000000000..ecfb6a91f --- /dev/null +++ b/src/main/java/roomescape/service/ReservationService.java @@ -0,0 +1,35 @@ +package roomescape.service; + +import org.springframework.stereotype.Service; +import roomescape.domain.InMemoryReservationRepository; +import roomescape.domain.Reservation; +import roomescape.dto.ReservationRequest; +import roomescape.dto.ReservationResponse; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ReservationService { + + private final InMemoryReservationRepository reservationRepository = new InMemoryReservationRepository(); + + public List findAll() { + return reservationRepository.findAll().stream() + .map(ReservationResponse::from) + .collect(Collectors.toList()); + } + + public ReservationResponse create(ReservationRequest request) { + Reservation newReservation = Reservation.of( + request.name(), request.parseDate(), request.parseTime() + ); + Reservation storedReservation = reservationRepository.save(newReservation); + return ReservationResponse.from(storedReservation); + } + + public void delete(Long id) { + reservationRepository.deleteById(id); + } +} + diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 4de9ed1d6..dd966d20b 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -70,4 +70,26 @@ public class MissionStepTest { .statusCode(200) .body("size()", is(0)); } + + @Test + void 사단계() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", ""); + params.put("time", ""); + + // 필요한 인자가 없는 경우 + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(400); + + // 삭제할 예약이 없는 경우 + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(400); + } } From c21871a404c84cc0b0c4717ebc5b9e41b4fc5037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Mon, 2 Jun 2025 12:14:52 +0900 Subject: [PATCH 05/27] =?UTF-8?q?refactor(review)=20:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomescapeController.java | 19 ++++++++++--------- .../domain/InMemoryReservationRepository.java | 4 ++-- .../java/roomescape/domain/Reservation.java | 2 +- .../roomescape/dto/ReservationRequest.java | 6 +++--- .../InvalidReservationRequestException.java | 7 ------- .../exception/ReservationException.java | 7 +++++++ .../ReservationExceptionHandler.java | 9 ++------- .../ReservationNotFoundException.java | 7 ------- .../service/ReservationService.java | 2 +- 9 files changed, 26 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/roomescape/exception/InvalidReservationRequestException.java create mode 100644 src/main/java/roomescape/exception/ReservationException.java delete mode 100644 src/main/java/roomescape/exception/ReservationNotFoundException.java diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java index f3e46ae08..72efa44f5 100644 --- a/src/main/java/roomescape/controller/RoomescapeController.java +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -6,7 +6,6 @@ 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.ResponseBody; import org.springframework.web.bind.annotation.RestController; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; @@ -25,25 +24,27 @@ public RoomescapeController(ReservationService reservationService) { } @GetMapping("/") - public String home() { - return "home"; + public ResponseEntity home() { + return ResponseEntity.ok("home"); } @GetMapping("/reservation") - public String reservationPage() { - return "reservation"; + public ResponseEntity reservationPage() { + return ResponseEntity.ok("reservation"); } @GetMapping("/reservations") - @ResponseBody - public List getReservations() { - return reservationService.findAll(); + public ResponseEntity> getReservations() { + List reservations = reservationService.findAll(); + return ResponseEntity.ok(reservations); } @PostMapping("/reservations") public ResponseEntity createReservation(@RequestBody ReservationRequest request) { ReservationResponse reservation = reservationService.create(request); - return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); + return ResponseEntity + .created(URI.create("/reservations/" + reservation.id())) + .body(reservation); } @DeleteMapping("/reservations/{id}") diff --git a/src/main/java/roomescape/domain/InMemoryReservationRepository.java b/src/main/java/roomescape/domain/InMemoryReservationRepository.java index 180b29fd9..cbc4c3de8 100644 --- a/src/main/java/roomescape/domain/InMemoryReservationRepository.java +++ b/src/main/java/roomescape/domain/InMemoryReservationRepository.java @@ -1,7 +1,7 @@ package roomescape.domain; import org.springframework.stereotype.Repository; -import roomescape.exception.ReservationNotFoundException; +import roomescape.exception.ReservationException; import java.util.ArrayList; import java.util.Collections; @@ -27,7 +27,7 @@ public Reservation save(Reservation reservation) { public void deleteById(Long id) { Reservation reservation = findById(id) - .orElseThrow(() -> new ReservationNotFoundException("[ERROR] 예약을 찾을 수 없습니다.")); + .orElseThrow(() -> new ReservationException("[ERROR] 예약을 찾을 수 없습니다.")); reservations.remove(reservation); } diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java index fc3daacd5..23af0b7b2 100644 --- a/src/main/java/roomescape/domain/Reservation.java +++ b/src/main/java/roomescape/domain/Reservation.java @@ -21,7 +21,7 @@ public static Reservation of(Long id, String name, LocalDate date, LocalTime tim return new Reservation(id, name, date, time); } - public static Reservation of(String name, LocalDate date, LocalTime time) { + public static Reservation create(String name, LocalDate date, LocalTime time) { return new Reservation(null, name, date, time); } diff --git a/src/main/java/roomescape/dto/ReservationRequest.java b/src/main/java/roomescape/dto/ReservationRequest.java index bbc17099c..b022d89d5 100644 --- a/src/main/java/roomescape/dto/ReservationRequest.java +++ b/src/main/java/roomescape/dto/ReservationRequest.java @@ -1,6 +1,6 @@ package roomescape.dto; -import roomescape.exception.InvalidReservationRequestException; +import roomescape.exception.ReservationException; import java.time.LocalDate; import java.time.LocalTime; @@ -12,7 +12,7 @@ public LocalDate parseDate() { try { return LocalDate.parse(date); } catch (DateTimeParseException e) { - throw new InvalidReservationRequestException("[ERROR] 날짜 형식이 잘못되었습니다."); + throw new ReservationException("[ERROR] 날짜 형식이 잘못되었습니다."); } } @@ -20,7 +20,7 @@ public LocalTime parseTime() { try { return LocalTime.parse(time); } catch (DateTimeParseException e) { - throw new InvalidReservationRequestException("[ERROR] 시간 형식이 잘못되었습니다."); + throw new ReservationException("[ERROR] 시간 형식이 잘못되었습니다."); } } } diff --git a/src/main/java/roomescape/exception/InvalidReservationRequestException.java b/src/main/java/roomescape/exception/InvalidReservationRequestException.java deleted file mode 100644 index 8bccf3a54..000000000 --- a/src/main/java/roomescape/exception/InvalidReservationRequestException.java +++ /dev/null @@ -1,7 +0,0 @@ -package roomescape.exception; - -public class InvalidReservationRequestException extends RuntimeException { - public InvalidReservationRequestException(String message) { - super(message); - } -} diff --git a/src/main/java/roomescape/exception/ReservationException.java b/src/main/java/roomescape/exception/ReservationException.java new file mode 100644 index 000000000..35178e682 --- /dev/null +++ b/src/main/java/roomescape/exception/ReservationException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class ReservationException extends RuntimeException { + public ReservationException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/exception/ReservationExceptionHandler.java b/src/main/java/roomescape/exception/ReservationExceptionHandler.java index 5939f7f29..987664607 100644 --- a/src/main/java/roomescape/exception/ReservationExceptionHandler.java +++ b/src/main/java/roomescape/exception/ReservationExceptionHandler.java @@ -7,13 +7,8 @@ @RestControllerAdvice public class ReservationExceptionHandler { - @ExceptionHandler(InvalidReservationRequestException.class) - public ResponseEntity handleInvalidReservation(InvalidReservationRequestException e) { - return ResponseEntity.badRequest().body(e.getMessage()); - } - - @ExceptionHandler(ReservationNotFoundException.class) - public ResponseEntity handleNotFound(ReservationNotFoundException e) { + @ExceptionHandler(ReservationException.class) + public ResponseEntity handleReservationException(ReservationException e) { return ResponseEntity.badRequest().body(e.getMessage()); } } diff --git a/src/main/java/roomescape/exception/ReservationNotFoundException.java b/src/main/java/roomescape/exception/ReservationNotFoundException.java deleted file mode 100644 index a101dd0f7..000000000 --- a/src/main/java/roomescape/exception/ReservationNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package roomescape.exception; - -public class ReservationNotFoundException extends RuntimeException { - public ReservationNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index ecfb6a91f..99e6e6941 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -21,7 +21,7 @@ public List findAll() { } public ReservationResponse create(ReservationRequest request) { - Reservation newReservation = Reservation.of( + Reservation newReservation = Reservation.create( request.name(), request.parseDate(), request.parseTime() ); Reservation storedReservation = reservationRepository.save(newReservation); From a6663c6896be8791d7ac86ed83cf0dd66da094e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Thu, 5 Jun 2025 12:56:52 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat(jdbc)=20:=205=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ src/main/resources/application.properties | 6 ++++++ src/main/resources/schema.sql | 7 +++++++ src/test/java/roomescape/MissionStepTest.java | 20 +++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 src/main/resources/schema.sql diff --git a/build.gradle b/build.gradle index fb96eaae1..f145fb792 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' testImplementation 'org.hamcrest:hamcrest:2.2' + + runtimeOnly 'com.h2database:h2' } test { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29bb..4f3021ecb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.datasource.url=jdbc:h2:mem:database +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..6fcf908e7 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,7 @@ +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) +); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index dd966d20b..72131ec87 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -3,18 +3,27 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; 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; import org.springframework.test.annotation.DirtiesContext; +import java.sql.Connection; +import java.sql.SQLException; import java.util.HashMap; import java.util.Map; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hamcrest.Matchers.is; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test void 일단계() { RestAssured.given().log().all() @@ -92,4 +101,15 @@ public class MissionStepTest { .then().log().all() .statusCode(400); } + + @Test + void 오단계() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection).isNotNull(); + assertThat(connection.getCatalog()).isEqualTo("DATABASE"); + assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } From 0f3595f192b9c0576c1e4b9a85fe66d311f074e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Thu, 5 Jun 2025 13:41:04 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat(jdbc)=20:=206=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcTemplateReservationRepository.java | 29 +++++++++++++++++++ .../service/ReservationService.java | 8 ++++- src/test/java/roomescape/MissionStepTest.java | 17 +++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java diff --git a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java new file mode 100644 index 000000000..15b740eb0 --- /dev/null +++ b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java @@ -0,0 +1,29 @@ +package roomescape.domain; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class JdbcTemplateReservationRepository{ + + private final JdbcTemplate JdbcTemplate; + + public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) { + JdbcTemplate = jdbcTemplate; + } + + private final RowMapper reservationRowMapper = (rs, rowNum) -> Reservation.of( + rs.getLong("id"), + rs.getString("name"), + rs.getDate("date").toLocalDate(), + rs.getTime("time").toLocalTime() + ); + + public List findAll() { + String sql = "SELECT * FROM reservation"; + return JdbcTemplate.query(sql, reservationRowMapper); + } +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 99e6e6941..1a488998d 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Service; import roomescape.domain.InMemoryReservationRepository; +import roomescape.domain.JdbcTemplateReservationRepository; import roomescape.domain.Reservation; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; @@ -13,9 +14,14 @@ public class ReservationService { private final InMemoryReservationRepository reservationRepository = new InMemoryReservationRepository(); + private final JdbcTemplateReservationRepository jdbcRepository; + + public ReservationService(JdbcTemplateReservationRepository jdbcRepository) { + this.jdbcRepository = jdbcRepository; + } public List findAll() { - return reservationRepository.findAll().stream() + return jdbcRepository.findAll().stream() .map(ReservationResponse::from) .collect(Collectors.toList()); } diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 72131ec87..3ad3009d8 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -7,10 +7,12 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; +import roomescape.domain.Reservation; import java.sql.Connection; import java.sql.SQLException; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -112,4 +114,19 @@ public class MissionStepTest { throw new RuntimeException(e); } } + + @Test + void 육단계() { + jdbcTemplate.update("INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)", "브라운", "2023-08-05", "15:40"); + + List reservations = RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200).extract() + .jsonPath().getList(".", Reservation.class); + + Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + + assertThat(reservations.size()).isEqualTo(count); + } } From 01f4d7ed8ad648042c4030b109a70209a036bd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Thu, 5 Jun 2025 13:59:15 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat(jdbc)=20:=207=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/InMemoryReservationRepository.java | 45 ------------------- .../JdbcTemplateReservationRepository.java | 45 +++++++++++++++---- .../service/ReservationService.java | 6 +-- src/test/java/roomescape/MissionStepTest.java | 27 +++++++++++ 4 files changed, 66 insertions(+), 57 deletions(-) delete mode 100644 src/main/java/roomescape/domain/InMemoryReservationRepository.java diff --git a/src/main/java/roomescape/domain/InMemoryReservationRepository.java b/src/main/java/roomescape/domain/InMemoryReservationRepository.java deleted file mode 100644 index cbc4c3de8..000000000 --- a/src/main/java/roomescape/domain/InMemoryReservationRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package roomescape.domain; - -import org.springframework.stereotype.Repository; -import roomescape.exception.ReservationException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; - -@Repository -public class InMemoryReservationRepository { - private final List reservations = new ArrayList<>(); - private final AtomicLong sequence = new AtomicLong(0L); - - public Reservation save(Reservation reservation) { - Reservation storedReservation = Reservation.of( - sequence.incrementAndGet(), - reservation.getName(), - reservation.getDate(), - reservation.getTime() - ); - reservations.add(storedReservation); - return storedReservation; - } - - public void deleteById(Long id) { - Reservation reservation = findById(id) - .orElseThrow(() -> new ReservationException("[ERROR] 예약을 찾을 수 없습니다.")); - reservations.remove(reservation); - } - - public List findAll() { - return Collections.unmodifiableList(reservations); - } - - public Optional findById(Long id) { - return reservations.stream() - .filter(reservation -> reservation.getId().equals(id)) - .findFirst(); - } -} - - diff --git a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java index 15b740eb0..c59bc1f19 100644 --- a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java +++ b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java @@ -2,19 +2,19 @@ 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.exception.ReservationException; +import java.sql.PreparedStatement; +import java.sql.Statement; import java.util.List; @Repository -public class JdbcTemplateReservationRepository{ - - private final JdbcTemplate JdbcTemplate; - - public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) { - JdbcTemplate = jdbcTemplate; - } +public class JdbcTemplateReservationRepository { + private final JdbcTemplate jdbcTemplate; private final RowMapper reservationRowMapper = (rs, rowNum) -> Reservation.of( rs.getLong("id"), rs.getString("name"), @@ -22,8 +22,37 @@ public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) { rs.getTime("time").toLocalTime() ); + public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + public List findAll() { String sql = "SELECT * FROM reservation"; - return JdbcTemplate.query(sql, reservationRowMapper); + return jdbcTemplate.query(sql, reservationRowMapper); + } + + 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.getDate())); + ps.setTime(3, java.sql.Time.valueOf(reservation.getTime())); + return ps; + }, keyHolder); + + Long generatedId = keyHolder.getKey().longValue(); + return Reservation.of(generatedId, reservation.getName(), reservation.getDate(), reservation.getTime()); + } + + public void deleteById(Long id) { + String sql = "DELETE FROM reservation WHERE id = ?"; + int affectedRows = jdbcTemplate.update(sql, id); + if (affectedRows == 0) { + throw new ReservationException("[ERROR] 예약을 찾을 수 없습니다."); + } } } + diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 1a488998d..78b62bd94 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -1,7 +1,6 @@ package roomescape.service; import org.springframework.stereotype.Service; -import roomescape.domain.InMemoryReservationRepository; import roomescape.domain.JdbcTemplateReservationRepository; import roomescape.domain.Reservation; import roomescape.dto.ReservationRequest; @@ -13,7 +12,6 @@ @Service public class ReservationService { - private final InMemoryReservationRepository reservationRepository = new InMemoryReservationRepository(); private final JdbcTemplateReservationRepository jdbcRepository; public ReservationService(JdbcTemplateReservationRepository jdbcRepository) { @@ -30,12 +28,12 @@ public ReservationResponse create(ReservationRequest request) { Reservation newReservation = Reservation.create( request.name(), request.parseDate(), request.parseTime() ); - Reservation storedReservation = reservationRepository.save(newReservation); + Reservation storedReservation = jdbcRepository.save(newReservation); return ReservationResponse.from(storedReservation); } public void delete(Long id) { - reservationRepository.deleteById(id); + jdbcRepository.deleteById(id); } } diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 3ad3009d8..29da4a3e5 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -129,4 +129,31 @@ public class MissionStepTest { assertThat(reservations.size()).isEqualTo(count); } + + @Test + void 칠단계() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", "2023-08-05"); + params.put("time", "10:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .header("Location", "/reservations/1"); + + Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + assertThat(count).isEqualTo(1); + + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(204); + + Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + assertThat(countAfterDelete).isEqualTo(0); + } } From a585671811e70e69d9ae60b51591d5786bf73e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Thu, 5 Jun 2025 14:04:51 +0900 Subject: [PATCH 09/27] =?UTF-8?q?fix(JdbcTemplateReservationRepository)=20?= =?UTF-8?q?:=20SQL=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20Date,=20Time=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=EC=A0=81=20=ED=83=80=EC=9E=85=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/JdbcTemplateReservationRepository.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java index c59bc1f19..0e0e94fdb 100644 --- a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java +++ b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java @@ -7,8 +7,10 @@ import org.springframework.stereotype.Repository; import roomescape.exception.ReservationException; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.Statement; +import java.sql.Time; import java.util.List; @Repository @@ -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.getDate())); - ps.setTime(3, java.sql.Time.valueOf(reservation.getTime())); + ps.setDate(2, Date.valueOf(reservation.getDate())); + ps.setTime(3, Time.valueOf(reservation.getTime())); return ps; }, keyHolder); From 482a7ede175aeb74f93b616ac60ac3b67c1ce0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Sat, 7 Jun 2025 21:35:18 +0900 Subject: [PATCH 10/27] =?UTF-8?q?refactor(MVC)=20:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/service/ReservationService.java | 7 +++++-- src/test/java/roomescape/MissionStepTest.java | 15 +++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 99e6e6941..cc41c4919 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -12,7 +12,11 @@ @Service public class ReservationService { - private final InMemoryReservationRepository reservationRepository = new InMemoryReservationRepository(); + private final InMemoryReservationRepository reservationRepository; + + public ReservationService(InMemoryReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } public List findAll() { return reservationRepository.findAll().stream() @@ -32,4 +36,3 @@ public void delete(Long id) { reservationRepository.deleteById(id); } } - diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index dd966d20b..b76e6a81c 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -2,6 +2,7 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; @@ -16,7 +17,8 @@ public class MissionStepTest { @Test - void 일단계() { + @DisplayName("홈 페이지 접근 시 정상 응답을 반환한다") + void getHomePageReturnsOk() { RestAssured.given().log().all() .when().get("/") .then().log().all() @@ -24,7 +26,8 @@ public class MissionStepTest { } @Test - void 이단계() { + @DisplayName("예약 조회 페이지와 API가 정상적으로 동작한다") + void getReservationPageAndList() { RestAssured.given().log().all() .when().get("/reservation") .then().log().all() @@ -38,7 +41,8 @@ public class MissionStepTest { } @Test - void 삼단계() { + @DisplayName("예약을 생성하고 조회하고 삭제할 수 있다") + void createReadAndDeleteReservation() { Map params = new HashMap<>(); params.put("name", "오찌"); params.put("date", "2025-06-02"); @@ -72,13 +76,13 @@ public class MissionStepTest { } @Test - void 사단계() { + @DisplayName("유효하지 않은 예약 생성 또는 삭제 시 에러를 반환한다") + void createOrDeleteReservationWithInvalidInputReturnsError() { Map params = new HashMap<>(); params.put("name", "브라운"); params.put("date", ""); params.put("time", ""); - // 필요한 인자가 없는 경우 RestAssured.given().log().all() .contentType(ContentType.JSON) .body(params) @@ -86,7 +90,6 @@ public class MissionStepTest { .then().log().all() .statusCode(400); - // 삭제할 예약이 없는 경우 RestAssured.given().log().all() .when().delete("/reservations/1") .then().log().all() From c27aff1b9f136ca2963d94bf2b2868d3e2ad47d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Sat, 7 Jun 2025 21:40:18 +0900 Subject: [PATCH 11/27] =?UTF-8?q?refactor(MissionStepTest)=20:=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EC=97=AD=ED=95=A0=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20Dis?= =?UTF-8?q?playName=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/roomescape/MissionStepTest.java | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 29da4a3e5..57a4c4a43 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -2,6 +2,7 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; +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; @@ -27,7 +28,8 @@ public class MissionStepTest { @Test - void 일단계() { + @DisplayName("홈 페이지에 접근하면 200 OK를 반환한다") + void homePageReturnsOk() { RestAssured.given().log().all() .when().get("/") .then().log().all() @@ -35,7 +37,8 @@ public class MissionStepTest { } @Test - void 이단계() { + @DisplayName("예약 페이지와 예약 목록 조회가 정상 동작한다") + void reservationPageAndEmptyList() { RestAssured.given().log().all() .when().get("/reservation") .then().log().all() @@ -49,12 +52,14 @@ public class MissionStepTest { } @Test - void 삼단계() { + @DisplayName("예약을 생성, 조회, 삭제하는 전체 흐름이 정상 동작한다") + void createReadDeleteReservationFlow() { Map params = new HashMap<>(); params.put("name", "오찌"); params.put("date", "2025-06-02"); params.put("time", "17:00"); + // 예약 생성 RestAssured.given().log().all() .contentType(ContentType.JSON) .body(params) @@ -64,17 +69,20 @@ public class MissionStepTest { .header("Location", "/reservations/1") .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(204); + // 삭제 후 조회 RestAssured.given().log().all() .when().get("/reservations") .then().log().all() @@ -83,13 +91,14 @@ public class MissionStepTest { } @Test - void 사단계() { + @DisplayName("유효하지 않은 입력 또는 삭제 요청 시 400 에러를 반환한다") + void invalidReservationInputOrDeletion() { Map params = new HashMap<>(); params.put("name", "브라운"); params.put("date", ""); params.put("time", ""); - // 필요한 인자가 없는 경우 + // 입력값 누락 RestAssured.given().log().all() .contentType(ContentType.JSON) .body(params) @@ -97,7 +106,7 @@ public class MissionStepTest { .then().log().all() .statusCode(400); - // 삭제할 예약이 없는 경우 + // 존재하지 않는 예약 삭제 RestAssured.given().log().all() .when().delete("/reservations/1") .then().log().all() @@ -105,7 +114,8 @@ public class MissionStepTest { } @Test - void 오단계() { + @DisplayName("DB 연결 및 reservation 테이블이 존재하는지 확인한다") + void validateDatabaseConnectionAndTableExists() { try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { assertThat(connection).isNotNull(); assertThat(connection.getCatalog()).isEqualTo("DATABASE"); @@ -116,13 +126,15 @@ public class MissionStepTest { } @Test - void 육단계() { + @DisplayName("DB에 직접 저장한 예약과 API로 조회한 예약 수가 일치한다") + void reservationCountMatchesBetweenDbAndApi() { jdbcTemplate.update("INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)", "브라운", "2023-08-05", "15:40"); List reservations = RestAssured.given().log().all() .when().get("/reservations") .then().log().all() - .statusCode(200).extract() + .statusCode(200) + .extract() .jsonPath().getList(".", Reservation.class); Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); @@ -131,12 +143,14 @@ public class MissionStepTest { } @Test - void 칠단계() { + @DisplayName("예약 추가 후 삭제하면 DB에 반영된 예약 수가 0이 된다") + void createThenDeleteReservationAndVerifyDatabase() { Map params = new HashMap<>(); params.put("name", "브라운"); params.put("date", "2023-08-05"); params.put("time", "10:00"); + // 생성 RestAssured.given().log().all() .contentType(ContentType.JSON) .body(params) @@ -148,6 +162,7 @@ public class MissionStepTest { Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); assertThat(count).isEqualTo(1); + // 삭제 RestAssured.given().log().all() .when().delete("/reservations/1") .then().log().all() From f350c55564cde8b0d0eb6fd29dda88eafa6ee475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Mon, 16 Jun 2025 10:37:06 +0900 Subject: [PATCH 12/27] =?UTF-8?q?refactor(repository)=20:=20=20repository?= =?UTF-8?q?=20,=20service=20=EA=B3=84=EC=B8=B5=20=EC=B1=85=EC=9E=84=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 --- .../domain/JdbcTemplateReservationRepository.java | 9 ++------- src/main/java/roomescape/service/ReservationService.java | 7 +++++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java index 0e0e94fdb..fe59e1eb8 100644 --- a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java +++ b/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java @@ -5,7 +5,6 @@ import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; -import roomescape.exception.ReservationException; import java.sql.Date; import java.sql.PreparedStatement; @@ -49,12 +48,8 @@ public Reservation save(Reservation reservation) { return Reservation.of(generatedId, reservation.getName(), reservation.getDate(), reservation.getTime()); } - public void deleteById(Long id) { + public int deleteById(Long id) { String sql = "DELETE FROM reservation WHERE id = ?"; - int affectedRows = jdbcTemplate.update(sql, id); - if (affectedRows == 0) { - throw new ReservationException("[ERROR] 예약을 찾을 수 없습니다."); - } + return jdbcTemplate.update(sql, id); } } - diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 78b62bd94..f26cce953 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -5,6 +5,7 @@ import roomescape.domain.Reservation; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; +import roomescape.exception.ReservationException; import java.util.List; import java.util.stream.Collectors; @@ -33,7 +34,9 @@ public ReservationResponse create(ReservationRequest request) { } public void delete(Long id) { - jdbcRepository.deleteById(id); + int affectedRows = jdbcRepository.deleteById(id); + if (affectedRows == 0) { + throw new ReservationException("[ERROR] 삭제하려는 예약을 찾을 수 없습니다."); + } } } - From 5a6428dedf129c4210fe2693dc8c36e7a1246479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Mon, 16 Jun 2025 10:43:20 +0900 Subject: [PATCH 13/27] =?UTF-8?q?refactor(service):=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=99=80=20=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=97=AD=ED=95=A0=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/service/ReservationService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index f26cce953..2ddb224a6 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -13,14 +13,14 @@ @Service public class ReservationService { - private final JdbcTemplateReservationRepository jdbcRepository; + private final JdbcTemplateReservationRepository reservationRepository; - public ReservationService(JdbcTemplateReservationRepository jdbcRepository) { - this.jdbcRepository = jdbcRepository; + public ReservationService(JdbcTemplateReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; } public List findAll() { - return jdbcRepository.findAll().stream() + return reservationRepository.findAll().stream() .map(ReservationResponse::from) .collect(Collectors.toList()); } @@ -29,12 +29,12 @@ public ReservationResponse create(ReservationRequest request) { Reservation newReservation = Reservation.create( request.name(), request.parseDate(), request.parseTime() ); - Reservation storedReservation = jdbcRepository.save(newReservation); + Reservation storedReservation = reservationRepository.save(newReservation); return ReservationResponse.from(storedReservation); } public void delete(Long id) { - int affectedRows = jdbcRepository.deleteById(id); + int affectedRows = reservationRepository.deleteById(id); if (affectedRows == 0) { throw new ReservationException("[ERROR] 삭제하려는 예약을 찾을 수 없습니다."); } From 7454741c8518bde759c62287fdbb95577488d671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Mon, 16 Jun 2025 10:47:42 +0900 Subject: [PATCH 14/27] =?UTF-8?q?fix(db):=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=83=80=EC=9E=85=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 6fcf908e7..564e3a40c 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,7 +1,7 @@ CREATE TABLE reservation ( id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - date VARCHAR(255) NOT NULL, - time VARCHAR(255) NOT NULL, + name VARCHAR(30) NOT NULL, + date DATE NOT NULL, + time TIME NOT NULL, PRIMARY KEY (id) ); From 2b14936f4f44ae74a42385f774ec5013b00b127b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Mon, 16 Jun 2025 10:52:22 +0900 Subject: [PATCH 15/27] =?UTF-8?q?refactor=20:=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=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/dto/ReservationResponse.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/roomescape/dto/ReservationResponse.java b/src/main/java/roomescape/dto/ReservationResponse.java index 4fa8e071e..4693a7a80 100644 --- a/src/main/java/roomescape/dto/ReservationResponse.java +++ b/src/main/java/roomescape/dto/ReservationResponse.java @@ -3,20 +3,22 @@ import roomescape.domain.Reservation; import java.time.LocalDate; -import java.time.LocalTime; +import java.time.format.DateTimeFormatter; public record ReservationResponse( Long id, String name, LocalDate date, - LocalTime time + String time ) { public static ReservationResponse from(Reservation reservation) { + String formattedTime = reservation.getTime().format(DateTimeFormatter.ofPattern("HH:mm")); + return new ReservationResponse( reservation.getId(), reservation.getName(), reservation.getDate(), - reservation.getTime() + formattedTime ); } } From c1342fc799129771996d002e952f56cc6b14d20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Mon, 16 Jun 2025 10:53:44 +0900 Subject: [PATCH 16/27] =?UTF-8?q?fix(web):=20API=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomescapeController.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java index 72efa44f5..1ce09b5ff 100644 --- a/src/main/java/roomescape/controller/RoomescapeController.java +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -1,12 +1,13 @@ package roomescape.controller; import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; 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.RestController; +import org.springframework.web.bind.annotation.ResponseBody; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; import roomescape.service.ReservationService; @@ -14,7 +15,7 @@ import java.net.URI; import java.util.List; -@RestController +@Controller public class RoomescapeController { private final ReservationService reservationService; @@ -24,22 +25,24 @@ public RoomescapeController(ReservationService reservationService) { } @GetMapping("/") - public ResponseEntity home() { - return ResponseEntity.ok("home"); + public String home() { + return "home"; } @GetMapping("/reservation") - public ResponseEntity reservationPage() { - return ResponseEntity.ok("reservation"); + public String reservationPage() { + return "reservation"; } @GetMapping("/reservations") + @ResponseBody public ResponseEntity> getReservations() { List reservations = reservationService.findAll(); return ResponseEntity.ok(reservations); } @PostMapping("/reservations") + @ResponseBody public ResponseEntity createReservation(@RequestBody ReservationRequest request) { ReservationResponse reservation = reservationService.create(request); return ResponseEntity @@ -48,6 +51,7 @@ public ResponseEntity createReservation(@RequestBody Reserv } @DeleteMapping("/reservations/{id}") + @ResponseBody public ResponseEntity deleteReservation(@PathVariable Long id) { reservationService.delete(id); return ResponseEntity.noContent().build(); From 912fc5d0eeb580d5b6a36997da59cd21a5be7a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Tue, 17 Jun 2025 10:43:10 +0900 Subject: [PATCH 17/27] merge(spring-jdbc) : merge pre step --- src/main/java/roomescape/domain/Reservation.java | 1 - .../java/roomescape/service/ReservationService.java | 12 ++++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java index 23af0b7b2..1a37e49bc 100644 --- a/src/main/java/roomescape/domain/Reservation.java +++ b/src/main/java/roomescape/domain/Reservation.java @@ -65,4 +65,3 @@ private void validateTime(LocalTime time) { } } } - diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index cc41c4919..2ddb224a6 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -1,10 +1,11 @@ package roomescape.service; import org.springframework.stereotype.Service; -import roomescape.domain.InMemoryReservationRepository; +import roomescape.domain.JdbcTemplateReservationRepository; import roomescape.domain.Reservation; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; +import roomescape.exception.ReservationException; import java.util.List; import java.util.stream.Collectors; @@ -12,9 +13,9 @@ @Service public class ReservationService { - private final InMemoryReservationRepository reservationRepository; + private final JdbcTemplateReservationRepository reservationRepository; - public ReservationService(InMemoryReservationRepository reservationRepository) { + public ReservationService(JdbcTemplateReservationRepository reservationRepository) { this.reservationRepository = reservationRepository; } @@ -33,6 +34,9 @@ public ReservationResponse create(ReservationRequest request) { } public void delete(Long id) { - reservationRepository.deleteById(id); + int affectedRows = reservationRepository.deleteById(id); + if (affectedRows == 0) { + throw new ReservationException("[ERROR] 삭제하려는 예약을 찾을 수 없습니다."); + } } } From 0bbfde8fd35945106b656e562510fa2b09fd29f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Tue, 17 Jun 2025 17:22:27 +0900 Subject: [PATCH 18/27] =?UTF-8?q?feat(step8)=20:=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomescapeController.java | 36 +++++++++++-- .../roomescape/domain/ReservationTime.java | 44 ++++++++++++++++ .../domain/ReservationTimeRepository.java | 51 +++++++++++++++++++ .../dto/ReservationTimeRequest.java | 4 ++ .../dto/ReservationTimeResponse.java | 13 +++++ .../service/ReservationService.java | 50 ++++++++++++++---- src/main/resources/schema.sql | 7 +++ src/main/resources/templates/home.html | 2 +- .../resources/templates/new-reservation.html | 2 +- src/main/resources/templates/reservation.html | 2 +- src/main/resources/templates/time.html | 2 +- src/test/java/roomescape/MissionStepTest.java | 32 ++++++++++++ 12 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 src/main/java/roomescape/domain/ReservationTime.java create mode 100644 src/main/java/roomescape/domain/ReservationTimeRepository.java create mode 100644 src/main/java/roomescape/dto/ReservationTimeRequest.java create mode 100644 src/main/java/roomescape/dto/ReservationTimeResponse.java diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java index 1ce09b5ff..167225abc 100644 --- a/src/main/java/roomescape/controller/RoomescapeController.java +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; +import roomescape.dto.ReservationTimeRequest; +import roomescape.dto.ReservationTimeResponse; import roomescape.service.ReservationService; import java.net.URI; @@ -34,17 +36,22 @@ public String reservationPage() { return "reservation"; } + @GetMapping("/time") + public String reservationTimePage() { + return "time"; + } + @GetMapping("/reservations") @ResponseBody public ResponseEntity> getReservations() { - List reservations = reservationService.findAll(); + List reservations = reservationService.findAllReservations(); return ResponseEntity.ok(reservations); } @PostMapping("/reservations") @ResponseBody public ResponseEntity createReservation(@RequestBody ReservationRequest request) { - ReservationResponse reservation = reservationService.create(request); + ReservationResponse reservation = reservationService.createReservation(request); return ResponseEntity .created(URI.create("/reservations/" + reservation.id())) .body(reservation); @@ -53,7 +60,30 @@ public ResponseEntity createReservation(@RequestBody Reserv @DeleteMapping("/reservations/{id}") @ResponseBody public ResponseEntity deleteReservation(@PathVariable Long id) { - reservationService.delete(id); + reservationService.deleteReservation(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/times") + @ResponseBody + public ResponseEntity createTime(@RequestBody ReservationTimeRequest request) { + ReservationTimeResponse time = reservationService.createTime(request); + return ResponseEntity + .created(URI.create("/times/" + time.id())) + .body(time); + } + + @GetMapping("/times") + @ResponseBody + public ResponseEntity> getTimes() { + List times = reservationService.findAllTimes(); + return ResponseEntity.ok(times); + } + + @DeleteMapping("/times/{id}") + @ResponseBody + public ResponseEntity deleteTime(@PathVariable Long id) { + reservationService.deleteTime(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java new file mode 100644 index 000000000..ade281a44 --- /dev/null +++ b/src/main/java/roomescape/domain/ReservationTime.java @@ -0,0 +1,44 @@ +package roomescape.domain; + +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +public class ReservationTime { + private final Long id; + private final LocalTime time; + + public ReservationTime(Long id, LocalTime time) { + validate(time); + this.id = id; + this.time = time; + } + + public static ReservationTime from(String time) { + return new ReservationTime(null, parseTime(time)); + } + + private static LocalTime parseTime(String time) { + if (time == null || time.isBlank()) { + throw new IllegalArgumentException("[ERROR] 시간은 비어있을 수 없습니다."); + } + try { + return LocalTime.parse(time); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("[ERROR] 시간 형식이 올바르지 않습니다. (HH:mm)"); + } + } + + private void validate(LocalTime time) { + if (time == null) { + throw new IllegalArgumentException("[ERROR] 시간은 필수 입력 항목입니다."); + } + } + + public Long getId() { + return id; + } + + public LocalTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/domain/ReservationTimeRepository.java b/src/main/java/roomescape/domain/ReservationTimeRepository.java new file mode 100644 index 000000000..dd2b7f81a --- /dev/null +++ b/src/main/java/roomescape/domain/ReservationTimeRepository.java @@ -0,0 +1,51 @@ +package roomescape.domain; + +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 java.sql.PreparedStatement; +import java.sql.Statement; +import java.sql.Time; +import java.util.List; + +@Repository +public class ReservationTimeRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper timeRowMapper = (rs, rowNum) -> new ReservationTime( + rs.getLong("id"), + rs.getTime("time").toLocalTime() + ); + + public ReservationTimeRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + String sql = "SELECT id, time FROM reservation_time"; + return jdbcTemplate.query(sql, timeRowMapper); + } + + public ReservationTime save(ReservationTime reservationTime) { + String sql = "INSERT INTO reservation_time (time) VALUES (?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setTime(1, Time.valueOf(reservationTime.getTime())); + return ps; + }, keyHolder); + + Long generatedId = keyHolder.getKey().longValue(); + return new ReservationTime(generatedId, reservationTime.getTime()); + } + + public int deleteById(Long id) { + String sql = "DELETE FROM reservation_time WHERE id = ?"; + return jdbcTemplate.update(sql, id); + } +} diff --git a/src/main/java/roomescape/dto/ReservationTimeRequest.java b/src/main/java/roomescape/dto/ReservationTimeRequest.java new file mode 100644 index 000000000..4638cdf02 --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationTimeRequest.java @@ -0,0 +1,4 @@ +package roomescape.dto; + +public record ReservationTimeRequest(String time) { +} diff --git a/src/main/java/roomescape/dto/ReservationTimeResponse.java b/src/main/java/roomescape/dto/ReservationTimeResponse.java new file mode 100644 index 000000000..b9e2fc2f2 --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationTimeResponse.java @@ -0,0 +1,13 @@ +package roomescape.dto; + +import roomescape.domain.ReservationTime; + +import java.time.format.DateTimeFormatter; + +public record ReservationTimeResponse(Long id, String time) { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + public static ReservationTimeResponse from(ReservationTime reservationTime) { + return new ReservationTimeResponse(reservationTime.getId(), reservationTime.getTime().format(FORMATTER)); + } +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 2ddb224a6..8d9cede1c 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -1,31 +1,35 @@ package roomescape.service; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.domain.JdbcTemplateReservationRepository; import roomescape.domain.Reservation; +import roomescape.domain.ReservationTime; +import roomescape.domain.ReservationTimeRepository; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; +import roomescape.dto.ReservationTimeRequest; +import roomescape.dto.ReservationTimeResponse; import roomescape.exception.ReservationException; import java.util.List; import java.util.stream.Collectors; @Service +@Transactional(readOnly = true) public class ReservationService { private final JdbcTemplateReservationRepository reservationRepository; + private final ReservationTimeRepository reservationTimeRepository; - public ReservationService(JdbcTemplateReservationRepository reservationRepository) { + public ReservationService(JdbcTemplateReservationRepository reservationRepository, + ReservationTimeRepository reservationTimeRepository) { this.reservationRepository = reservationRepository; + this.reservationTimeRepository = reservationTimeRepository; } - public List findAll() { - return reservationRepository.findAll().stream() - .map(ReservationResponse::from) - .collect(Collectors.toList()); - } - - public ReservationResponse create(ReservationRequest request) { + @Transactional + public ReservationResponse createReservation(ReservationRequest request) { Reservation newReservation = Reservation.create( request.name(), request.parseDate(), request.parseTime() ); @@ -33,10 +37,38 @@ public ReservationResponse create(ReservationRequest request) { return ReservationResponse.from(storedReservation); } - public void delete(Long id) { + public List findAllReservations() { + return reservationRepository.findAll().stream() + .map(ReservationResponse::from) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteReservation(Long id) { int affectedRows = reservationRepository.deleteById(id); if (affectedRows == 0) { throw new ReservationException("[ERROR] 삭제하려는 예약을 찾을 수 없습니다."); } } + + @Transactional + public ReservationTimeResponse createTime(ReservationTimeRequest request) { + ReservationTime newReservationTime = ReservationTime.from(request.time()); + ReservationTime savedReservationTime = reservationTimeRepository.save(newReservationTime); + return ReservationTimeResponse.from(savedReservationTime); + } + + public List findAllTimes() { + return reservationTimeRepository.findAll().stream() + .map(ReservationTimeResponse::from) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteTime(Long id) { + int affectedRows = reservationTimeRepository.deleteById(id); + if (affectedRows == 0) { + throw new IllegalArgumentException("[ERROR] 삭제하려는 시간을 찾을 수 없습니다."); + } + } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 564e3a40c..4ad5d8966 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -5,3 +5,10 @@ CREATE TABLE reservation ( time TIME NOT NULL, PRIMARY KEY (id) ); + +CREATE TABLE reservation_time +( + id BIGINT NOT NULL AUTO_INCREMENT, + time TIME NOT NULL, + PRIMARY KEY (id) +); diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 7ddf752a6..0f4abbb39 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -45,4 +45,4 @@

어드민 페이지

- \ No newline at end of file + diff --git a/src/main/resources/templates/new-reservation.html b/src/main/resources/templates/new-reservation.html index ae49f5ad0..58dff48f1 100644 --- a/src/main/resources/templates/new-reservation.html +++ b/src/main/resources/templates/new-reservation.html @@ -66,4 +66,4 @@

예약 관리

- \ No newline at end of file + diff --git a/src/main/resources/templates/reservation.html b/src/main/resources/templates/reservation.html index 7c0eb9fc4..cc1623463 100644 --- a/src/main/resources/templates/reservation.html +++ b/src/main/resources/templates/reservation.html @@ -66,4 +66,4 @@

예약 관리

- \ No newline at end of file + diff --git a/src/main/resources/templates/time.html b/src/main/resources/templates/time.html index 4670aeb0f..d190e6611 100644 --- a/src/main/resources/templates/time.html +++ b/src/main/resources/templates/time.html @@ -63,4 +63,4 @@

시간 관리

- \ No newline at end of file + diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index b76e6a81c..5701bac55 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -95,4 +95,36 @@ void createOrDeleteReservationWithInvalidInputReturnsError() { .then().log().all() .statusCode(400); } + + @Test + @DisplayName("시간을 생성하고 조회하고 삭제할 수 있다") + void createReadAndDeleteTime() { + Map params = new HashMap<>(); + params.put("time", "10:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/times") + .then().log().all() + .statusCode(201) + .header("Location", "/times/1"); + + RestAssured.given().log().all() + .when().get("/times") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + + RestAssured.given().log().all() + .when().delete("/times/1") + .then().log().all() + .statusCode(204); + + RestAssured.given().log().all() + .when().get("/times") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } } From 66fd8cf27c73aa9d21d310b1664a9d9be5ede956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8C=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=89=E1=85=AE=E1=86=AB?= Date: Wed, 18 Jun 2025 22:46:06 +0900 Subject: [PATCH 19/27] =?UTF-8?q?feat(step9)=20:=209=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomescapeController.java | 12 +-- .../java/roomescape/domain/Reservation.java | 59 ++++++--------- ...sitory.java => ReservationRepository.java} | 39 ++++++---- .../roomescape/domain/ReservationTime.java | 44 ----------- .../domain/ReservationTimeRepository.java | 51 ------------- src/main/java/roomescape/domain/Time.java | 31 ++++++++ .../roomescape/domain/TimeRepository.java | 73 +++++++++++++++++++ .../roomescape/dto/ReservationRequest.java | 23 ++++-- .../roomescape/dto/ReservationResponse.java | 7 +- .../dto/ReservationTimeRequest.java | 4 - .../dto/ReservationTimeResponse.java | 13 ---- src/main/java/roomescape/dto/TimeRequest.java | 20 +++++ .../java/roomescape/dto/TimeResponse.java | 13 ++++ .../service/ReservationService.java | 65 ++++++++++------- src/main/resources/schema.sql | 22 +++--- src/test/java/roomescape/MissionStepTest.java | 37 ++++++++-- 16 files changed, 296 insertions(+), 217 deletions(-) rename src/main/java/roomescape/domain/{JdbcTemplateReservationRepository.java => ReservationRepository.java} (57%) delete mode 100644 src/main/java/roomescape/domain/ReservationTime.java delete mode 100644 src/main/java/roomescape/domain/ReservationTimeRepository.java create mode 100644 src/main/java/roomescape/domain/Time.java create mode 100644 src/main/java/roomescape/domain/TimeRepository.java delete mode 100644 src/main/java/roomescape/dto/ReservationTimeRequest.java delete mode 100644 src/main/java/roomescape/dto/ReservationTimeResponse.java create mode 100644 src/main/java/roomescape/dto/TimeRequest.java create mode 100644 src/main/java/roomescape/dto/TimeResponse.java diff --git a/src/main/java/roomescape/controller/RoomescapeController.java b/src/main/java/roomescape/controller/RoomescapeController.java index 167225abc..e9051ff85 100644 --- a/src/main/java/roomescape/controller/RoomescapeController.java +++ b/src/main/java/roomescape/controller/RoomescapeController.java @@ -10,8 +10,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import roomescape.dto.ReservationRequest; import roomescape.dto.ReservationResponse; -import roomescape.dto.ReservationTimeRequest; -import roomescape.dto.ReservationTimeResponse; +import roomescape.dto.TimeRequest; +import roomescape.dto.TimeResponse; import roomescape.service.ReservationService; import java.net.URI; @@ -66,8 +66,8 @@ public ResponseEntity deleteReservation(@PathVariable Long id) { @PostMapping("/times") @ResponseBody - public ResponseEntity createTime(@RequestBody ReservationTimeRequest request) { - ReservationTimeResponse time = reservationService.createTime(request); + public ResponseEntity createTime(@RequestBody TimeRequest request) { + TimeResponse time = reservationService.createTime(request); return ResponseEntity .created(URI.create("/times/" + time.id())) .body(time); @@ -75,8 +75,8 @@ public ResponseEntity createTime(@RequestBody Reservati @GetMapping("/times") @ResponseBody - public ResponseEntity> getTimes() { - List times = reservationService.findAllTimes(); + public ResponseEntity> getTimes() { + List times = reservationService.findAllTimes(); return ResponseEntity.ok(times); } diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java index 1a37e49bc..6c83ba1cb 100644 --- a/src/main/java/roomescape/domain/Reservation.java +++ b/src/main/java/roomescape/domain/Reservation.java @@ -1,15 +1,16 @@ package roomescape.domain; +import roomescape.exception.ReservationException; + import java.time.LocalDate; -import java.time.LocalTime; public class Reservation { private final Long id; private final String name; private final LocalDate date; - private final LocalTime time; + private final Time time; - private Reservation(Long id, String name, LocalDate date, LocalTime time) { + private Reservation(Long id, String name, LocalDate date, Time time) { validate(name, date, time); this.id = id; this.name = name; @@ -17,51 +18,39 @@ private Reservation(Long id, String name, LocalDate date, LocalTime time) { this.time = time; } - public static Reservation of(Long id, String name, LocalDate date, LocalTime time) { - return new Reservation(id, name, date, time); - } - - public static Reservation create(String name, LocalDate date, LocalTime time) { + public static Reservation create(String name, LocalDate date, Time time) { return new Reservation(null, name, date, time); } - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public LocalDate getDate() { - return date; - } - - public LocalTime getTime() { - return time; + public static Reservation of(Long id, String name, LocalDate date, Time time) { + return new Reservation(id, name, date, time); } - private void validate(String name, LocalDate date, LocalTime time) { - validateName(name); - validateDate(date); - validateTime(time); + private void validate(String name, LocalDate date, Time time) { + validateRequiredFields(name, date, time); + validateBusinessRules(date); } - private void validateName(String name) { + private void validateRequiredFields(String name, LocalDate date, Time time) { if (name == null || name.isBlank()) { - throw new IllegalArgumentException("[ERROR] 이름은 필수 입력 항목입니다."); + throw new ReservationException("[ERROR] name 필드는 비어있을 수 없습니다."); } - } - - private void validateDate(LocalDate date) { if (date == null) { - throw new IllegalArgumentException("[ERROR] 날짜는 필수 입력 항목입니다."); + throw new ReservationException("[ERROR] date 필드는 null일 수 없습니다."); + } + if (time == null) { + throw new ReservationException("[ERROR] time 필드는 null일 수 없습니다."); } } - private void validateTime(LocalTime time) { - if (time == null) { - throw new IllegalArgumentException("[ERROR] 시간은 필수 입력 항목입니다."); + private void validateBusinessRules(LocalDate date) { + if (date.isBefore(LocalDate.now())) { + throw new ReservationException("[ERROR] 예약 날짜는 과거일 수 없습니다."); } } + + public Long getId() { return id; } + public String getName() { return name; } + public LocalDate getDate() { return date; } + public Time getTime() { return time; } } diff --git a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/domain/ReservationRepository.java similarity index 57% rename from src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java rename to src/main/java/roomescape/domain/ReservationRepository.java index fe59e1eb8..e65daf01c 100644 --- a/src/main/java/roomescape/domain/JdbcTemplateReservationRepository.java +++ b/src/main/java/roomescape/domain/ReservationRepository.java @@ -9,38 +9,51 @@ import java.sql.Date; import java.sql.PreparedStatement; import java.sql.Statement; -import java.sql.Time; +import java.time.LocalDate; import java.util.List; @Repository -public class JdbcTemplateReservationRepository { +public class ReservationRepository { private final JdbcTemplate jdbcTemplate; - private final RowMapper reservationRowMapper = (rs, rowNum) -> Reservation.of( - rs.getLong("id"), - rs.getString("name"), - rs.getDate("date").toLocalDate(), - rs.getTime("time").toLocalTime() - ); - - public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) { + + private final RowMapper reservationRowMapper = (rs, rowNum) -> { + Time time = Time.of( + rs.getLong("time_id"), + rs.getTime("time_value").toLocalTime() + ); + return Reservation.of( + rs.getLong("reservation_id"), + rs.getString("name"), + rs.getDate("date").toLocalDate(), + time + ); + }; + + public ReservationRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + public boolean existsByDateAndTimeId(LocalDate date, Long timeId) { + String sql = "SELECT EXISTS(SELECT 1 FROM reservation WHERE date = ? AND time_id = ?)"; + return jdbcTemplate.queryForObject(sql, Boolean.class, date, timeId); + } + public List findAll() { - String sql = "SELECT * FROM reservation"; + String sql = "SELECT r.id as reservation_id, r.name, r.date, t.id as time_id, t.time as time_value " + + "FROM reservation as r inner join time as t on r.time_id = t.id"; return jdbcTemplate.query(sql, reservationRowMapper); } public Reservation save(Reservation reservation) { - String sql = "INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)"; + String sql = "INSERT INTO reservation (name, date, time_id) 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.getDate())); - ps.setTime(3, Time.valueOf(reservation.getTime())); + ps.setLong(3, reservation.getTime().getId()); return ps; }, keyHolder); diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java deleted file mode 100644 index ade281a44..000000000 --- a/src/main/java/roomescape/domain/ReservationTime.java +++ /dev/null @@ -1,44 +0,0 @@ -package roomescape.domain; - -import java.time.LocalTime; -import java.time.format.DateTimeParseException; - -public class ReservationTime { - private final Long id; - private final LocalTime time; - - public ReservationTime(Long id, LocalTime time) { - validate(time); - this.id = id; - this.time = time; - } - - public static ReservationTime from(String time) { - return new ReservationTime(null, parseTime(time)); - } - - private static LocalTime parseTime(String time) { - if (time == null || time.isBlank()) { - throw new IllegalArgumentException("[ERROR] 시간은 비어있을 수 없습니다."); - } - try { - return LocalTime.parse(time); - } catch (DateTimeParseException e) { - throw new IllegalArgumentException("[ERROR] 시간 형식이 올바르지 않습니다. (HH:mm)"); - } - } - - private void validate(LocalTime time) { - if (time == null) { - throw new IllegalArgumentException("[ERROR] 시간은 필수 입력 항목입니다."); - } - } - - public Long getId() { - return id; - } - - public LocalTime getTime() { - return time; - } -} diff --git a/src/main/java/roomescape/domain/ReservationTimeRepository.java b/src/main/java/roomescape/domain/ReservationTimeRepository.java deleted file mode 100644 index dd2b7f81a..000000000 --- a/src/main/java/roomescape/domain/ReservationTimeRepository.java +++ /dev/null @@ -1,51 +0,0 @@ -package roomescape.domain; - -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 java.sql.PreparedStatement; -import java.sql.Statement; -import java.sql.Time; -import java.util.List; - -@Repository -public class ReservationTimeRepository { - - private final JdbcTemplate jdbcTemplate; - - private final RowMapper timeRowMapper = (rs, rowNum) -> new ReservationTime( - rs.getLong("id"), - rs.getTime("time").toLocalTime() - ); - - public ReservationTimeRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - String sql = "SELECT id, time FROM reservation_time"; - return jdbcTemplate.query(sql, timeRowMapper); - } - - public ReservationTime save(ReservationTime reservationTime) { - String sql = "INSERT INTO reservation_time (time) VALUES (?)"; - KeyHolder keyHolder = new GeneratedKeyHolder(); - - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); - ps.setTime(1, Time.valueOf(reservationTime.getTime())); - return ps; - }, keyHolder); - - Long generatedId = keyHolder.getKey().longValue(); - return new ReservationTime(generatedId, reservationTime.getTime()); - } - - public int deleteById(Long id) { - String sql = "DELETE FROM reservation_time WHERE id = ?"; - return jdbcTemplate.update(sql, id); - } -} diff --git a/src/main/java/roomescape/domain/Time.java b/src/main/java/roomescape/domain/Time.java new file mode 100644 index 000000000..cf4a9f5ce --- /dev/null +++ b/src/main/java/roomescape/domain/Time.java @@ -0,0 +1,31 @@ +package roomescape.domain; + +import java.time.LocalTime; + +public class Time { + private final Long id; + private final LocalTime time; + + private Time(Long id, LocalTime time) { + validate(time); + this.id = id; + this.time = time; + } + + public static Time create(LocalTime time) { + return new Time(null, time); + } + + public static Time of(Long id, LocalTime time) { + return new Time(id, time); + } + + private void validate(LocalTime time) { + if (time == null) { + throw new IllegalArgumentException("Time의 time 필드는 null일 수 없습니다."); + } + } + + public Long getId() { return id; } + public LocalTime getTime() { return time; } +} diff --git a/src/main/java/roomescape/domain/TimeRepository.java b/src/main/java/roomescape/domain/TimeRepository.java new file mode 100644 index 000000000..e020a6fa6 --- /dev/null +++ b/src/main/java/roomescape/domain/TimeRepository.java @@ -0,0 +1,73 @@ +package roomescape.domain; + +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 java.sql.PreparedStatement; +import java.sql.Statement; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +@Repository +public class TimeRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper