diff --git a/build.gradle b/build.gradle index 40a581b1..efbf2716 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,11 @@ 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' + runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.assertj:assertj-core:3.20.2' } test { diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java index 6badd1a1..380c485e 100644 --- a/src/main/java/roomescape/controller/ReservationController.java +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -1,77 +1,62 @@ package roomescape.controller; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import org.springframework.web.context.request.WebRequest; import roomescape.exception.InvalidReservationException; -import roomescape.exception.NotFoundReservationException; import roomescape.model.Reservation; +import roomescape.respository.ReservationRepository; import java.net.URI; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -@Controller +@RestController +@RequestMapping("/reservations") public class ReservationController { + private final ReservationRepository reservationRepository; - private List reservations = new ArrayList<>(); - - private AtomicLong index = new AtomicLong(1); - - @GetMapping("/reservation") - public String reservation() { - return "reservation"; + public ReservationController(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; } - @GetMapping("/reservations") + @GetMapping @ResponseBody public List getReservations() { - return reservations; + return reservationRepository.findAllReservations(); } - @PostMapping("/reservations") + @GetMapping("/{id}") + @ResponseBody + public Reservation findReservationById(@PathVariable Long id) { + return reservationRepository.findReservationById(id); + } + + @PostMapping @ResponseBody public ResponseEntity createReservation(@RequestBody Reservation reservation) { validateReservation(reservation); - Long id = index.getAndIncrement(); - String name = reservation.getName(); - String date = reservation.getDate(); - String time = reservation.getTime(); - - Reservation newReservation = new Reservation(id, name, date, time); - reservations.add(newReservation); + Reservation newReservation = reservationRepository.insert(reservation); return ResponseEntity.created(URI.create("/reservations/" + newReservation.getId())).body(newReservation); } - @DeleteMapping("/reservations/{id}") + @DeleteMapping("/{id}") @ResponseBody public ResponseEntity deleteReservation(@PathVariable Long id) { - boolean removed = reservations.removeIf(r -> r.getId() == id); - if (!removed) { - throw new NotFoundReservationException("예약을 찾을 수 없습니다: " + id); - } + reservationRepository.delete(id); return ResponseEntity.noContent().build(); } - @ExceptionHandler({InvalidReservationException.class, NotFoundReservationException.class}) - public ResponseEntity handleException(RuntimeException e, WebRequest request) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - private void validateReservation(Reservation reservation) { if (reservation.getName() == null || reservation.getName().isEmpty()) { - throw new InvalidReservationException("이름이 필요합니다."); - } + throw new InvalidReservationException("name", "이름이 필요합니다."); + } if (reservation.getDate() == null || reservation.getDate().isEmpty()) { - throw new InvalidReservationException("날짜가 필요합니다."); - } + throw new InvalidReservationException("date", "날짜가 필요합니다."); + } if (reservation.getTime() == null || reservation.getTime().isEmpty()) { - throw new InvalidReservationException("시간이 필요합니다"); - } + throw new InvalidReservationException("time", "시간이 필요합니다."); + } } + } diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..562dd5d4 --- /dev/null +++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java @@ -0,0 +1,29 @@ +package roomescape.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundReservationException.class) + public ResponseEntity handleNotFoundReservationException(NotFoundReservationException e, WebRequest request) { + String message = "예약을 찾을 수 없습니다: " + e.getReservationId(); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body((e.getMessage())); + } + + @ExceptionHandler(InvalidReservationException.class) + public ResponseEntity handleInvalidReservationException(InvalidReservationException e, WebRequest request) { + String message = e.getFieldName() + "'의 값이 유용하지 않습니다: " + e.getMessage(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("예기치 않은 오류가 발생했습니다: " + e.getMessage()); + } + +} diff --git a/src/main/java/roomescape/exception/InvalidReservationException.java b/src/main/java/roomescape/exception/InvalidReservationException.java index 3af57ac2..d23ff602 100644 --- a/src/main/java/roomescape/exception/InvalidReservationException.java +++ b/src/main/java/roomescape/exception/InvalidReservationException.java @@ -1,7 +1,15 @@ package roomescape.exception; public class InvalidReservationException extends RuntimeException{ - public InvalidReservationException(String message) { + + private String fieldName; + + public InvalidReservationException(String fieldName, String message) { super(message); + this.fieldName = fieldName; + } + + public String getFieldName() { + return fieldName; } } diff --git a/src/main/java/roomescape/exception/NotFoundReservationException.java b/src/main/java/roomescape/exception/NotFoundReservationException.java index 0a214534..bc2b9efe 100644 --- a/src/main/java/roomescape/exception/NotFoundReservationException.java +++ b/src/main/java/roomescape/exception/NotFoundReservationException.java @@ -1,7 +1,15 @@ package roomescape.exception; public class NotFoundReservationException extends RuntimeException{ - public NotFoundReservationException(String message) { - super(message); + + private Long reservationId; + + public NotFoundReservationException(Long reservationId) { + super("예약을 찾을 수 없습니다: " + reservationId); + this.reservationId = reservationId; + } + + public Long getReservationId() { + return reservationId; } } diff --git a/src/main/java/roomescape/model/Reservation.java b/src/main/java/roomescape/model/Reservation.java index 64eeb422..d32fadde 100644 --- a/src/main/java/roomescape/model/Reservation.java +++ b/src/main/java/roomescape/model/Reservation.java @@ -27,5 +27,4 @@ public String getDate() { public String getTime() { return time; - } } diff --git a/src/main/java/roomescape/respository/ReservationRepository.java b/src/main/java/roomescape/respository/ReservationRepository.java new file mode 100644 index 00000000..882c39ac --- /dev/null +++ b/src/main/java/roomescape/respository/ReservationRepository.java @@ -0,0 +1,72 @@ +package roomescape.respository; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.exception.NotFoundReservationException; +import roomescape.model.Reservation; + +import java.sql.PreparedStatement; +import java.util.List; + +@Repository +public class ReservationRepository { + + private JdbcTemplate jdbcTemplate; + + public ReservationRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + private final RowMapper rowMapper = (resultSet, rowNum) -> { + Reservation reservation = new Reservation( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("date"), + resultSet.getString("time")); + return reservation; + }; + + public List findAllReservations() { + String sql = "SELECT id, name, date, time FROM reservation"; + return jdbcTemplate.query(sql, rowMapper); + } + + public Reservation findReservationById(Long id) { + String sql = "SELECT id, name, date, time FROM reservation where id = ?"; + try { + return jdbcTemplate.queryForObject(sql, rowMapper, id); + } catch (EmptyResultDataAccessException e) { + throw new NotFoundReservationException(id); + } + } + + public Reservation insert(Reservation reservation) { + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO reservation(name, date, time) VALUES (?, ?, ?)", new String[]{"id"}); + ps.setString(1, reservation.getName()); + ps.setString(2, reservation.getDate()); + ps.setString(3, reservation.getTime()); + return ps; + }, keyHolder); + + Long id = keyHolder.getKey().longValue(); + return new Reservation(id, reservation.getName(), reservation.getDate(), reservation.getTime()); + } + + public int delete(Long id) { + String sql = "DELETE FROM reservation WHERE id = ?"; + int row = jdbcTemplate.update(sql, id); + if (row == 0) { + throw new NotFoundReservationException(id); + } + return row; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29b..dc4a6e8c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.datasource.url=jdbc:h2:mem:database + + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 00000000..8d9ab275 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date VARCHAR(255) NOT NULL, + time VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 173deadf..4fdf60ee 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -4,8 +4,20 @@ 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; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; +import roomescape.model.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.Assertions.assertThat; +import static org.hamcrest.Matchers.is; import java.util.HashMap; import java.util.Map; @@ -16,6 +28,9 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + @Autowired + private JdbcTemplate jdbcTemplate; + @DisplayName("루트 경로 확인") @Test void 일단계() { @@ -28,11 +43,6 @@ public class MissionStepTest { @DisplayName("예약 조회 확인") @Test void 이단계() { - RestAssured.given().log().all() - .when().get("/reservation") - .then().log().all() - .statusCode(200); - RestAssured.given().log().all() .when().get("/reservations") .then().log().all() @@ -74,6 +84,7 @@ public class MissionStepTest { .statusCode(200) .body("size()", is(0)); } + @DisplayName("예외 처리 확인") @Test void 사단계() { @@ -94,6 +105,62 @@ public class MissionStepTest { RestAssured.given().log().all() .when().delete("/reservations/1") .then().log().all() - .statusCode(400); + .statusCode(404); + } + + @DisplayName("데이터 베이스 연결 및 테이블 확인") + @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); + } + } + + @DisplayName("데이터 조회 확인") + @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); + } + + @DisplayName("데이터 추가 및 삭제 확인") + @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); } }