diff --git a/build.gradle b/build.gradle index 57267157c..e4a27b7a9 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' + + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + runtimeOnly 'com.h2database:h2' } test { diff --git a/src/main/java/roomescape/controller/HomeController.java b/src/main/java/roomescape/controller/HomeController.java new file mode 100644 index 000000000..6efe2ab58 --- /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.RequestMapping; + +@Controller +public class HomeController { + + @RequestMapping("/") + public String home() { + return "home"; + } + +} diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java new file mode 100644 index 000000000..c9dbb157e --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -0,0 +1,58 @@ +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.dto.ReservationRequestDto; +import roomescape.dto.ReservationResponseDto; +import roomescape.service.ReservationService; + +import java.net.URI; +import java.util.List; + +@Controller +public class ReservationController { + + private final ReservationService reservationService; + + public ReservationController(ReservationService reservationService) { + this.reservationService = reservationService; + } + + // 홈화면 + @GetMapping("/reservation") + public String reservationPage() { + return "reservation"; + } + + //예약 조회 + @ResponseBody + @GetMapping("/reservations") + public ResponseEntity> list() { + List reservations = reservationService.getAllReservations(); + return ResponseEntity.ok(reservations); + } + + //예약 추가 + @ResponseBody + @PostMapping("/reservations") + public ResponseEntity create(@RequestBody ReservationRequestDto newReservationDto) { + + ReservationResponseDto reservation = reservationService.createReservation(newReservationDto); + + return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())) + .body(reservation); + } + + //예약 삭제 + @DeleteMapping("/reservations/{id}") + public ResponseEntity delete(@PathVariable Long id) { + reservationService.deleteReservation(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/dao/ReservationDao.java b/src/main/java/roomescape/dao/ReservationDao.java new file mode 100644 index 000000000..a942ca7a3 --- /dev/null +++ b/src/main/java/roomescape/dao/ReservationDao.java @@ -0,0 +1,45 @@ +package roomescape.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import roomescape.entity.Reservation; + +import java.util.List; + +@Repository +public class ReservationDao { + + private final JdbcTemplate jdbcTemplate; + + public ReservationDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll(){ + String sql = "SELECT id, name, date, time FROM reservation"; + + return jdbcTemplate.query( + sql, (resultSet, rowNum) -> new Reservation( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("date"), + resultSet.getTimestamp("time").toLocalDateTime() + )); + } + + public Reservation insert(Reservation reservation) { + String sql = "INSERT INTO reservation(name, date, time) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, reservation.getName(), reservation.getDate(), reservation.getTime()); + + String query = "SELECT id FROM reservation ORDER BY id DESC LIMIT 1"; + Long id = jdbcTemplate.queryForObject(query, Long.class); + + return new Reservation(id, reservation.getName(), reservation.getDate(), reservation.getTime()); + } + + public void delete(Long id) { + String sql = "DELETE FROM reservation WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + +} diff --git a/src/main/java/roomescape/dto/ReservationRequestDto.java b/src/main/java/roomescape/dto/ReservationRequestDto.java new file mode 100644 index 000000000..b96ba1f69 --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationRequestDto.java @@ -0,0 +1,27 @@ +package roomescape.dto; + +import java.time.LocalDateTime; + +public class ReservationRequestDto { + private String name; + private String date; + private LocalDateTime time; + + public ReservationRequestDto(String name, String date, LocalDateTime time) { + this.name = name; + this.date = date; + this.time = time; + } + + public String getName() { + return name; + } + + public String getDate() { + return date; + } + + public LocalDateTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/dto/ReservationResponseDto.java b/src/main/java/roomescape/dto/ReservationResponseDto.java new file mode 100644 index 000000000..3ad189302 --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationResponseDto.java @@ -0,0 +1,34 @@ +package roomescape.dto; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public class ReservationResponseDto { + private Long id; + private String name; + private String date; + private LocalDateTime time; + + public ReservationResponseDto(Long id, String name, String date, LocalDateTime time) { + this.id = id; + this.name = name; + this.date = date; + this.time = time; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDate() { + return date; + } + + public LocalDateTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/entity/Reservation.java b/src/main/java/roomescape/entity/Reservation.java new file mode 100644 index 000000000..30138b6f3 --- /dev/null +++ b/src/main/java/roomescape/entity/Reservation.java @@ -0,0 +1,37 @@ +package roomescape.entity; + +import java.time.LocalDateTime; + +public class Reservation { + private Long id; + private String name; + private String date; + private LocalDateTime time; + + public Reservation(Long id, String name, String date, LocalDateTime time) { + this.id = id; + this.name = name; + this.date = date; + this.time = time; + } + + public Reservation(String name, String date, LocalDateTime time) { + this(null, name, date, time); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDate() { + return date; + } + + public LocalDateTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/exception/ExceptionHandler.java b/src/main/java/roomescape/exception/ExceptionHandler.java new file mode 100644 index 000000000..8d83f2506 --- /dev/null +++ b/src/main/java/roomescape/exception/ExceptionHandler.java @@ -0,0 +1,19 @@ +package roomescape.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ExceptionHandler { + + @org.springframework.web.bind.annotation.ExceptionHandler(NotFoundReservationException.class) + public ResponseEntity handleNotFoundReservationException(NotFoundReservationException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(InvalidValueException.class) + public ResponseEntity handleInvalidReservationException(InvalidValueException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); + } +} diff --git a/src/main/java/roomescape/exception/InvalidValueException.java b/src/main/java/roomescape/exception/InvalidValueException.java new file mode 100644 index 000000000..0fe7afbaa --- /dev/null +++ b/src/main/java/roomescape/exception/InvalidValueException.java @@ -0,0 +1,8 @@ +package roomescape.exception; + +public class InvalidValueException extends RuntimeException { + + public InvalidValueException(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..55990b696 --- /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/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java new file mode 100644 index 000000000..920916f21 --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -0,0 +1,34 @@ +package roomescape.repository; + +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import roomescape.dao.ReservationDao; +import roomescape.entity.Reservation; + +import java.util.List; + +@Repository +public class ReservationRepository { + + private final ReservationDao reservationDao; + + public ReservationRepository(ReservationDao reservationDao) { + this.reservationDao = reservationDao; + } + + public List findAll() { + return reservationDao.findAll(); + } + + //예약 추가 + public Reservation insert(Reservation reservation) { + return reservationDao.insert(reservation); + } + + //예약 삭제 + public void delete(Long id) { + reservationDao.delete(id); + } +} + diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java new file mode 100644 index 000000000..88e0193f6 --- /dev/null +++ b/src/main/java/roomescape/service/ReservationService.java @@ -0,0 +1,51 @@ +package roomescape.service; + +import org.springframework.stereotype.Service; +import roomescape.dto.ReservationRequestDto; +import roomescape.dto.ReservationResponseDto; +import roomescape.entity.Reservation; +import roomescape.repository.ReservationRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ReservationService { + + private final ReservationRepository reservationRepository; + + public ReservationService(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } + + public List getAllReservations(){ + return reservationRepository.findAll() + .stream() + .map(this::toResponseDto) + .collect(Collectors.toList()); + } + + public ReservationResponseDto createReservation(ReservationRequestDto requestDto){ + + Reservation reservation = new Reservation( + requestDto.getName(), + requestDto.getDate(), + requestDto.getTime() + ); + Reservation savedReservation = reservationRepository.insert(reservation); + return toResponseDto(savedReservation); + } + + public void deleteReservation(Long id){ + reservationRepository.delete(id); + } + + private ReservationResponseDto toResponseDto(Reservation reservation) { + return new ReservationResponseDto( + reservation.getId(), + reservation.getName(), + reservation.getDate(), + reservation.getTime() + ); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29bb..39bfef124 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.port=8080 +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 000000000..55063b774 --- /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 Timestamp NOT NULL, + PRIMARY KEY (id) +); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index cf4efbe91..c2fd98592 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -1,10 +1,16 @@ 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) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { @@ -16,4 +22,76 @@ 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 갯수 만큼 검증하거나 0개임을 확인하세요. + } + + @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); + } + + } diff --git a/src/test/java/roomescape/MissionStepTest2.java b/src/test/java/roomescape/MissionStepTest2.java new file mode 100644 index 000000000..b738a5143 --- /dev/null +++ b/src/test/java/roomescape/MissionStepTest2.java @@ -0,0 +1,80 @@ +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.entity.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; +import static org.hamcrest.Matchers.is; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class MissionStepTest2 { + + @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(".", Reservation.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); + } +}