Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/main/java/roomescape/AdminController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package roomescape;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나도 1~3 단계에선 하지 않았지만 API 명세라던가 기능구현 목록을 추가해주는게 좋을 거 같아!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

응! 리뷰어한테 같은 피드백을 받았어!
그래서 9단계까지 끝내고 README에 명세서를 추가했어!


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin")
public class AdminController {

@GetMapping
public String showMainPage() {
return "index";
}

@GetMapping("/reservation")
public String showReservationPage() {
return "admin/reservation-legacy";
}

}
13 changes: 13 additions & 0 deletions src/main/java/roomescape/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape;

public record Reservation(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reservation을 record로 만든 이유가 궁금해

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 Reservation의 역할은 DTO와 크게 다르지 않다고 생각해.
클라이언트에서 받은 정보를 그대로 DB에게 주고, DB에게 받은 정보를 그대로 클라이언트에게 넘기니까

이후에 추가적인 요구사항이나, 추가적인 작업이 필요하다면 class로 바꿀 것 같아.

Long id,
String name,
String date,
String time) {

public Reservation toEntity(Long id) {
return new Reservation(id, this.name, this.date, this.time);
}

}
43 changes: 43 additions & 0 deletions src/main/java/roomescape/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package roomescape;

import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/reservations")
public class ReservationController {

private final ReservationRepository reservationRepository;

public ReservationController(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}

@GetMapping
public ResponseEntity<List<Reservation>> listReservations() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listReservations 보다 좀 더 좋은 이름이 있지 않을까?
모든 예약을 반환해준다는 의미를 전달해주면 좋을 거 같아

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.slipp.net/questions/79 -> 포비(자바지기)에게 이야기 해보세요.

는 농담이고, 네이밍 컨벤션을 찾아봤는데 위 자료가 보였고, 납득 가능한 것 같아서 사용했어
(여기서 list는 동사형으로 list의 의미로 봤을 떄)

return ResponseEntity.ok(reservationRepository.findAll());
}

@PostMapping
public ResponseEntity<Reservation> createReservation(@RequestBody Reservation reservation) {
Reservation newReservation = reservationRepository.create(reservation);

URI location = URI.create("/reservations/" + newReservation.id());
return ResponseEntity.created(location).body(newReservation);
Comment on lines +33 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 나는 뭔가 문자열을 + 연산하는게 껄끄럽다고 해야하나 암튼 그래서 아래처럼 처리했는데 맘에 드는 방식대로 해도 괜찮을 듯!

@PostMapping
    public ResponseEntity<ReservationResponse> save(@RequestBody final ReservationRequest reservationRequest) {
        final ReservationResponse reservationResponse = reservationService.save(reservationRequest);
        final URI uri = UriComponentsBuilder.fromPath("/reservations/{id}")
                .buildAndExpand(reservationResponse.id())
                .toUri();
        return ResponseEntity.created(uri)
                .body(reservationResponse);
    }

}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteReservation(@PathVariable Long id) {
reservationRepository.deleteById(id);
return ResponseEntity.noContent().build();
}

}
65 changes: 65 additions & 0 deletions src/main/java/roomescape/ReservationRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package roomescape;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
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;

@Repository
public class ReservationRepository {

private static final String TABLE_NAME = "reservation";
private static final RowMapper<Reservation> ROW_MAPPER = (resultSet, rowNum) -> new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getString("time"));

private final JdbcTemplate jdbcTemplate;

public ReservationRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public List<Reservation> findAll() {
return jdbcTemplate.query("SELECT id, name, date, time FROM %s".formatted(TABLE_NAME), ROW_MAPPER);
}

public Reservation create(Reservation reservation) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> insertQuery(connection, reservation), keyHolder);

Long id = keyHolder.getKey().longValue();
return reservation.toEntity(id);
}

private PreparedStatement insertQuery(Connection connection, Reservation reservation) throws SQLException {
PreparedStatement preparedStatement = connection.prepareStatement(
"INSERT INTO %s (name, date, time) VALUES (?, ?, ?)".formatted(TABLE_NAME), new String[]{"id"});
preparedStatement.setString(1, reservation.name());
preparedStatement.setString(2, reservation.date());
preparedStatement.setString(3, reservation.time());
return preparedStatement;
}

public void deleteById(Long id) {
Reservation foundReservation = findById(id);
jdbcTemplate.update("DELETE FROM %s WHERE id = ?".formatted(TABLE_NAME), foundReservation.id());
}

private Reservation findById(Long id) {
Reservation reservation = jdbcTemplate.queryForObject(
"SELECT id, name, date, time FROM %s WHERE id = ?".formatted(TABLE_NAME), ROW_MAPPER, id);

if (reservation == null) {
throw new IllegalStateException("해당 예약이 없습니다.");
}
return reservation;
}

}
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:database
8 changes: 8 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -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)
);
4 changes: 2 additions & 2 deletions src/main/resources/templates/admin/reservation-legacy.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/time">Time</a>
<a class="nav-link" href="/admin/time">Time</a>
</li>
</ul>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/templates/admin/reservation.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/time">Time</a>
<a class="nav-link" href="/admin/time">Time</a>
</li>
</ul>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<body>

<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">
<a class="navbar-brand" href="/static">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
Expand All @@ -22,10 +22,10 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/time">Time</a>
<a class="nav-link" href="/admin/time">Time</a>
</li>
</ul>
</div>
Expand Down
20 changes: 0 additions & 20 deletions src/test/java/roomescape/MissionStepTest.java

This file was deleted.

39 changes: 39 additions & 0 deletions src/test/java/roomescape/acceptance/AdminPageAcceptanceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package roomescape.acceptance;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 테스트가 LMS에서 제공하는 테스트 밖에 없는 거 같은데 다른 테스트도 추가해주!!!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 단위에서는 크게 복잡하지 않아서, 다른 테스트는 추가하지 않았음! 아마 다음 미션부터는 의식적으로 테스트를 짤 것 같아. 대신에 LMS에서 제공하는 테스트를 가공해서 작성했어!


import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AdminPageAcceptanceTest {

@LocalServerPort
int port;

@BeforeEach
public void setUp() {
RestAssured.port = port;
}

@DisplayName("어드민 메인 페이지 조회")
@Test
void get_welcomePage() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get 다음은 스네이크케이스고 welcomePage는 카멜케이스인데 통일해주!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일부러 통일 안했음!

get 요청으로 날린다는 것을 강조하기 위해, get_welcomPage() 라고 했어!

RestAssured.given().log().all()
.when().get("/admin")
.then().log().all()
.statusCode(200);
}

@DisplayName("어드민 예약 관리 페이지 조회")
@Test
void get_reservationPage() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

RestAssured.given().log().all()
.when().get("/admin/reservation")
.then().log().all()
.statusCode(200);
}

}
85 changes: 85 additions & 0 deletions src/test/java/roomescape/acceptance/ReservationAcceptanceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package roomescape.acceptance;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.jdbc.Sql;
import roomescape.Reservation;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SQL 애노테이션을 좀 더 쉽게 테스트 할 수 있겠군 👍 배워갑니다

class ReservationAcceptanceTest {

@LocalServerPort
private int port;
@Autowired
private JdbcTemplate jdbcTemplate;

@BeforeEach
public void setUp() {
RestAssured.port = port;
}

@DisplayName("전체 예약 조회")
@Test
void get_reservations() {
insertDefaultData();

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(1));
}

@DisplayName("예약 추가")
@Test
void post_reservation() {
Reservation reservation = new Reservation(null, "브라운", "2023-08-05", "15:40");
Reservation expectedReservation = new Reservation(1L, "브라운", "2023-08-05", "15:40");

Reservation createdReservation = RestAssured.given().log().all()
.contentType(ContentType.JSON).body(reservation)
.when().post("/reservations")
.then().log().all()
.statusCode(201)
.header("Location", "/reservations/1")
.extract().as(Reservation.class);

assertThat(createdReservation).isEqualTo(expectedReservation);
assertThat(countReservation()).isEqualTo(1);
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 createdReservation 처럼 아래 countReservation 메서드도 변수로 만든다음 테스트하는건 어떻게 생각해??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나는 거꾸로 assertThat(countReservation()).isEqualTo(1); 형식으로 통일했어!
이것 만으로도 삭제 후에 카운트했다는 것을 알 수 있지 않을까? (아닌가?)

}

@DisplayName("예약 삭제")
@Test
void delete_reservation() {
insertDefaultData();

RestAssured.given().log().all()
.when().delete("/reservations/1")
.then().log().all()
.statusCode(204);

Integer countAfterDelete = countReservation();
assertThat(countAfterDelete).isZero();
}

private void insertDefaultData() {
jdbcTemplate.update("INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)",
"브라운", "2023-08-05", "15:40");
}

private Integer countReservation() {
return jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from -> FROM

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이걸 놓치네... (나는 이런 부분이 약한듯...)

}

}
1 change: 1 addition & 0 deletions src/test/resources/truncate.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TRUNCATE TABLE reservation RESTART IDENTITY;