Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

켈리 1-6 단계 제출 #8

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
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
13 changes: 12 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ repositories {
}

dependencies {
Copy link
Member

Choose a reason for hiding this comment

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

주석을 달아두니 보기 좋군요

// Spring Boot Starter
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'

// Thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// JDBC
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

// h2
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// REST-Assured
testImplementation 'io.rest-assured:rest-assured:5.3.1'
}

Expand Down
12 changes: 12 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 웹 페이지 조회

- [X] 어드민 페이지 조회
- [X] 예약 페이지 조회

# API
Copy link
Member

Choose a reason for hiding this comment

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

켈리~
resource 마다 request, response 명세를 적어주면 더 좋을 것 같네요 👍


- [X] 예약 정보 전체 조회
- [X] 예약 정보 저장
- [X] 예약자 이름 검증
- 예약자 이름은 `1`이상 `5`이하 문자열만 허용
- [X] 예약 정보 삭제
22 changes: 22 additions & 0 deletions src/main/java/roomescape/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package roomescape.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import roomescape.repository.H2ReservationRepository;
import roomescape.repository.ReservationRepository;

@Configuration
public class AppConfig {

private final NamedParameterJdbcTemplate template;

public AppConfig(final NamedParameterJdbcTemplate template) {
this.template = template;
}

@Bean
Copy link
Member

Choose a reason for hiding this comment

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

@component 대신 @bean을 직접 등록한 이유가 있나요??
설명해주세요~

Copy link
Member

Choose a reason for hiding this comment

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

저도 궁금해요
기준이 있다면 pr description이나 readme에 적어주는 것은 어떨까요?!

public ReservationRepository reservationRepository() {
return new H2ReservationRepository(template);
}
}
44 changes: 44 additions & 0 deletions src/main/java/roomescape/controller/AdminApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package roomescape.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 roomescape.domain.Reservation;
import roomescape.dto.ReservationResponse;
import roomescape.dto.SaveReservationRequest;
import roomescape.repository.ReservationRepository;

import java.util.List;
Comment on lines +3 to +14
Copy link
Member

Choose a reason for hiding this comment

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

구글 자바 컨벤션에서 non-static import는 빈 줄 없이 한 블록으로 구분되어야 할 거에요~~~
참고 하세용


@RestController
public class AdminApiController {
Copy link
Member

Choose a reason for hiding this comment

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

이렇게 이름을 정한 이유는 현재 api가 reservation 관련 하나만 있어서인가요?
time 관련 api가 추가되고 이름을 어떻게 바꿨는지도 궁금하네요!

private final ReservationRepository reservationRepository;

public AdminApiController(final ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}

@GetMapping("/reservations")
public List<ReservationResponse> getReservations() {
return reservationRepository.findAll()
.stream()
.map(ReservationResponse::from)
.toList();
}

@PostMapping("/reservations")
public ReservationResponse saveReservation(@RequestBody final SaveReservationRequest request) {
Reservation reservation = request.toReservation();
Reservation savedReservation = reservationRepository.save(reservation);

return ReservationResponse.from(savedReservation);
}

@DeleteMapping("/reservations/{reservation-id}")
public void deleteReservation(@PathVariable("reservation-id") final Long reservationId) {
Copy link
Member

Choose a reason for hiding this comment

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

여기도 pathvariable value를 null을 받고 싶은 의도가 있으신건가요?

reservationRepository.deleteById(reservationId);
}
}
18 changes: 18 additions & 0 deletions src/main/java/roomescape/controller/AdminWebController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package roomescape.controller;

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

@Controller
public class AdminWebController {

Copy link
Member

Choose a reason for hiding this comment

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

클래스 선언 이후 공백 줄이 대부분 없는 것으로 보이는데,
혹시 공백 없이 바로 메서드나 필드를 쓰는 것이 본인의 컨벤션이라면 이 줄은 지우는게 좋겠네요~

@GetMapping("/admin")
public String getAdminPage() {
return "/admin/index";
}

@GetMapping("/admin/reservation")
public String getReservationPage() {
return "/admin/reservation-legacy";
}
}
22 changes: 22 additions & 0 deletions src/main/java/roomescape/domain/ClientName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package roomescape.domain;

public class ClientName {
Copy link
Member

Choose a reason for hiding this comment

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

1단계에서 학습한 부분을 잊지 않았군요. 꼼꼼하게 원시 값을 포장했네요 👍

private static final int MAXIMUM_ENABLE_NAME_LENGTH = 5;

private final String value;

public ClientName(final String value) {
validateClientName(value);
this.value = value;
}

private void validateClientName(final String value) {
if (value == null || value.isEmpty() || value.length() > MAXIMUM_ENABLE_NAME_LENGTH) {
throw new IllegalArgumentException("예약자 이름은 1글자 이상 5글자 이하여야 합니다.");
}
}

public String getValue() {
return value;
}
}
53 changes: 53 additions & 0 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package roomescape.domain;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class Reservation {
private static final Long DEFAULT_ID_VALUE = 0L;
Copy link
Member

Choose a reason for hiding this comment

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

이렇게 기본 값을 명시하면 어떤 점이 좋은지 궁금해요


private final Long id;
private final ClientName clientName;
private final LocalDate date;
private final LocalTime time;

public Reservation(final ClientName clientName, final LocalDate date, final LocalTime time) {
this(DEFAULT_ID_VALUE, clientName, date, time);
}

public Reservation(final Long id, final ClientName clientName, final LocalDate date, final LocalTime time) {
validateReservationDateAndTime(date, time);
this.id = id;
this.clientName = clientName;
this.date = date;
this.time = time;
}

private void validateReservationDateAndTime(final LocalDate date, final LocalTime time) {
LocalDateTime reservationLocalDateTime = LocalDateTime.of(date, time);
if (reservationLocalDateTime.isBefore(LocalDateTime.now())) {
Copy link
Member

Choose a reason for hiding this comment

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

이런 메소드가 있는 걸 처음 알았네요
이것도 예약자 이름 길이 검증처럼 readme에 함께 작성해주면 어떨까요?
코드 읽기 전 실행 먼저 해봤다가 예약 안되는줄...

throw new IllegalArgumentException("현재 날짜보다 이전 날짜를 예약할 수 없습니다.");
Comment on lines +27 to +30
Copy link
Member

Choose a reason for hiding this comment

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

오~ 이건 진짜 잘했는데요? 👍👍
베워갑니다

}
}

public Reservation initializeIndex(final Long reservationId) {
return new Reservation(reservationId, clientName, date, time);
}

public Long getId() {
return id;
}

public ClientName getClientName() {
return clientName;
}

public LocalDate getDate() {
return date;
}

public LocalTime getTime() {
return time;
}
}
17 changes: 17 additions & 0 deletions src/main/java/roomescape/dto/ReservationResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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) {
Copy link
Member

Choose a reason for hiding this comment

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

클라이언트 http 응답을 null을 주지는 않을 것 같은데 Long을 사용하신 이유가 있을까용

public static ReservationResponse from(final Reservation reservation) {
return new ReservationResponse(
reservation.getId(),
reservation.getClientName().getValue(),
reservation.getDate(),
reservation.getTime()
);
}
}
17 changes: 17 additions & 0 deletions src/main/java/roomescape/dto/SaveReservationRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package roomescape.dto;

import roomescape.domain.ClientName;
import roomescape.domain.Reservation;

import java.time.LocalDate;
import java.time.LocalTime;

public record SaveReservationRequest(LocalDate date, String name, LocalTime time) {
public Reservation toReservation() {
return new Reservation(
new ClientName(name),
date,
time
);
}
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.exception;

public record ErrorResponse(String message) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package roomescape.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.NoSuchElementException;

@RestControllerAdvice
public class ReservationExceptionHandler {

@ExceptionHandler
protected ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
Copy link
Member

Choose a reason for hiding this comment

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

요렇게 전달하면 어디서 어떻게 처리가 되나요???

}

@ExceptionHandler
protected ResponseEntity<ErrorResponse> handleNoSuchElementException(NoSuchElementException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
62 changes: 62 additions & 0 deletions src/main/java/roomescape/repository/H2ReservationRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package roomescape.repository;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import roomescape.domain.ClientName;
import roomescape.domain.Reservation;

import java.util.List;
import java.util.Map;

@Repository
public class H2ReservationRepository implements ReservationRepository {
private final NamedParameterJdbcTemplate template;

public H2ReservationRepository(final NamedParameterJdbcTemplate template) {
this.template = template;
}

@Override
public List<Reservation> findAll() {
String sql = "SELECT id, name, date, time FROM reservation";

List<Reservation> reservations = template.query(sql, itemRowMapper());
return reservations;
}

private RowMapper<Reservation> itemRowMapper() {
return ((rs, rowNum) -> new Reservation(
rs.getLong("id"),
new ClientName(rs.getString("name")),
rs.getDate("date").toLocalDate(),
rs.getTime("time").toLocalTime()
));
}

@Override
public Reservation save(final Reservation reservation) {
String sql = "INSERT INTO reservation(name, date, time) VALUES (:name, :date, :time)";
MapSqlParameterSource param = new MapSqlParameterSource()
.addValue("name", reservation.getClientName().getValue())
.addValue("date", reservation.getDate())
.addValue("time", reservation.getTime());
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);

long savedReservationId = keyHolder.getKey().longValue();

return reservation.initializeIndex(savedReservationId);
Copy link
Member

Choose a reason for hiding this comment

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

오호 이렇게도 할 수 있군요 👍
배워갑니다

}

@Override
public void deleteById(final Long reservationId) {
Copy link
Member

Choose a reason for hiding this comment

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

이 기능도 내부적으로 null을 필요로하는게 아니면 long이 옳다고 생각합니다. (내부 동작과 시그니처의 일관성)

String sql = "DELETE FROM reservation WHERE id = :id";
Map<String, Long> param = Map.of("id", reservationId);

template.update(sql, param);
}
}
13 changes: 13 additions & 0 deletions src/main/java/roomescape/repository/ReservationRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape.repository;

import roomescape.domain.Reservation;

import java.util.List;

public interface ReservationRepository {
Copy link
Member

Choose a reason for hiding this comment

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

인터페이스로 분리해둔 이유를 듣고 싶어요

List<Reservation> findAll();

Reservation save(Reservation reservation);

void deleteById(Long reservationId);
}
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
spring.h2.console.enabled=true
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)
);
9 changes: 4 additions & 5 deletions src/main/resources/templates/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>

Expand All @@ -22,10 +21,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-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
Loading