diff --git a/build.gradle b/build.gradle index 57267157c..458a76b48 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + runtimeOnly 'com.h2database:h2' } test { diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java new file mode 100644 index 000000000..78690a40d --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -0,0 +1,91 @@ +package roomescape.controller; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import roomescape.dto.ReservationAddReq; +import roomescape.dto.ReservationReq; +import roomescape.exception.InvalidRequestReservationException; +import roomescape.exception.NotFoundReservationException; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +@Controller +public class ReservationController { + + //AtomicLong: Long 자료형 가지는 Wrapping 클래스. incrementAndGet(): ++x + private AtomicLong index = new AtomicLong(0); + private List reservations1 = new ArrayList<>(); + private JdbcTemplate jdbcTemplate; + + public ReservationController(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @GetMapping("/reservation") + public String reservation(Model model){ + + model.addAttribute(reservations1); + return "reservation"; + } + + //예약 조회 + @GetMapping("/reservations") + @ResponseBody + public List reservations(){ + String sql="SELECT * from reservation"; + List reservations=jdbcTemplate.query( + sql, + (resultSet,rowNum)->{ + ReservationReq reservation = new ReservationReq( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("date"), + resultSet.getString("time") + ); + return reservation; + }); + return reservations; + } + + //예약 추가 + @PostMapping("/reservations") + @ResponseBody + @ResponseStatus(HttpStatus.CREATED) + public ReservationReq addReservation(@RequestBody ReservationAddReq reservation, HttpServletResponse response){ + if(reservation.getName().isEmpty()||reservation.getDate().isEmpty()||reservation.getTime().isEmpty()){ + throw new InvalidRequestReservationException("필요한 인자가 없습니다."); + } + + String sql="INSERT INTO reservation(name, date, time) VALUES (?,?,?)"; + jdbcTemplate.update(sql,reservation.getName(),reservation.getDate(),reservation.getTime()); + String getIdSql="SELECT LAST_INSERT_ID()"; //해당 구문 사용 위해 application.properties에 MODE=MySQL 추가함 + Long id=jdbcTemplate.queryForObject(getIdSql, Long.class); + ReservationReq newReservation =new ReservationReq(id,reservation.getName(),reservation.getDate(),reservation.getTime()); + + // ResponseEntity 사용하면 header 명시적으로 지정하지 않아도 된다고 한다. + response.setHeader("Location", "/reservations/" + id); + return newReservation; + } + + //예약 삭제 + @DeleteMapping("/reservations/{id}") + @ResponseBody + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteReservation(@PathVariable Long id){ + + String sql="DELETE FROM reservation WHERE id=?"; + int rowCount=jdbcTemplate.update(sql,id); + if(rowCount==0){ + throw new NotFoundReservationException("삭제할 예약이 없습니다"); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/roomescape/controller/StartController.java b/src/main/java/roomescape/controller/StartController.java new file mode 100644 index 000000000..73f23f94b --- /dev/null +++ b/src/main/java/roomescape/controller/StartController.java @@ -0,0 +1,18 @@ +package roomescape.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class StartController { + + @GetMapping("/") + public String home(){ + return "home"; + } + + + + +} diff --git a/src/main/java/roomescape/dto/ReservationAddReq.java b/src/main/java/roomescape/dto/ReservationAddReq.java new file mode 100644 index 000000000..a86225fbd --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationAddReq.java @@ -0,0 +1,12 @@ +package roomescape.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ReservationAddReq { + private String name; + private String date; //우선 String으로.. 차후 Date로 형변환 필요하면 교체.. + private String time; //동일!! +} diff --git a/src/main/java/roomescape/dto/ReservationReq.java b/src/main/java/roomescape/dto/ReservationReq.java new file mode 100644 index 000000000..542565146 --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationReq.java @@ -0,0 +1,17 @@ +package roomescape.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor //기본 생성자 자동 생성 +public class ReservationReq { + //캡슐화를 위해서는 private으로 해야 하지만, step12의 코드에서 private으로 하면 JSON이 접근을 못 해서 직렬화 못 하는 에러 발생 + //해결을 위해 lombok의 getter 추가 + private Long id; + private String name; + private String date; //우선 String으로.. 차후 Date로 형변환 필요하면 교체.. + private String time; //동일!! + + +} diff --git a/src/main/java/roomescape/exception/ExceptionHandlers.java b/src/main/java/roomescape/exception/ExceptionHandlers.java new file mode 100644 index 000000000..c9bed34e1 --- /dev/null +++ b/src/main/java/roomescape/exception/ExceptionHandlers.java @@ -0,0 +1,21 @@ +package roomescape.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + + +@ControllerAdvice +public class ExceptionHandlers { + + @ExceptionHandler(NotFoundReservationException.class) + public ResponseEntity NotFoundReservationException() { //딱히 메시지에 id 등 추가할 필요를 못 느껴서 매개변수 없앰. + return ResponseEntity.badRequest().build(); + } + + @ExceptionHandler(InvalidRequestReservationException.class) + public ResponseEntity InvalidRequestReservationException(){ + return ResponseEntity.badRequest().build(); + } + +} diff --git a/src/main/java/roomescape/exception/InvalidRequestReservationException.java b/src/main/java/roomescape/exception/InvalidRequestReservationException.java new file mode 100644 index 000000000..b34365062 --- /dev/null +++ b/src/main/java/roomescape/exception/InvalidRequestReservationException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class InvalidRequestReservationException extends RuntimeException { + public InvalidRequestReservationException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/exception/NotFoundReservationException.java b/src/main/java/roomescape/exception/NotFoundReservationException.java new file mode 100644 index 000000000..9aa1022b2 --- /dev/null +++ b/src/main/java/roomescape/exception/NotFoundReservationException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class NotFoundReservationException extends RuntimeException { + public NotFoundReservationException(String message) { + super(message); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29bb..d7553a520 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.datasource.url=jdbc:h2:mem:database;MODE=MySQL diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..8d9ab2754 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date VARCHAR(255) NOT NULL, + time VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index cf4efbe91..a11fd1925 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -1,9 +1,22 @@ package roomescape; 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 roomescape.dto.ReservationReq; + +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; +import static org.hamcrest.Matchers.is; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @@ -16,4 +29,132 @@ 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(0)); // 아직 생성 요청이 없으니 Controller에서 임의로 넣어준 Reservation 갯수(=1) 만큼 검증. + } + + @Test + void 삼단계() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", "2023-08-05"); + params.put("time", "15:40"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(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)); + } + + @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); + } + + @Autowired + private JdbcTemplate jdbcTemplate; + + @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); + } + } + + @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(".", ReservationReq.class); + + Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class); + + 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); + } + + }