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

Open
wants to merge 5 commits into
base: robinjoon
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
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 요구사항 문서

- [x] localhost:8080/admin 요청 시 어드민 메인 페이지가 응답할 수 있도록 구현한다.
- [x] 어드민 메인 페이지는 templates/admin/index.html 파일을 이용한다.
- [x] localhost:8080/admin/reservation 요청 시 아래 화면과 같이 예약 관리 페이지가 응답할 수 있도록 구현한다.
- [x] 페이지는 templates/admin/reservation-legacy.html 파일을 이용한다.
- [x] 예약 조회 API 명세를 따라 예약 관리 페이지 로드 시 호출되는 예약 목록 조회 API를 구현한다.
- [x] API 명세를 따라 예약 추가 API 와 삭제 API를 구현한다.
- [x] 예약 추가와 취소가 잘 동작한다.
- [x] 이상의 요구 사항을 데이터베이스와 연동하도록 한다.

# API 명세

## 예약 조회 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 명세를 꼼꼼하게 잘 작성해주셨네요 👍


### Request

> GET /reservations HTTP/1.1

### Response

> HTTP/1.1 200
>
> Content-Type: application/json

``` JSON
[
{
"id": 1,
"name": "브라운",
"date": "2023-01-01",
"time": "10:00"
},
{
"id": 2,
"name": "브라운",
"date": "2023-01-02",
"time": "11:00"
}
]
```
Comment on lines +16 to +41
Copy link
Member

Choose a reason for hiding this comment

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

자세한 명세 👍


## 예약 추가 API

### Request

> POST /reservations HTTP/1.1
>
> content-type: application/json

```JSON
{
"date": "2023-08-05",
"name": "브라운",
"time": "15:40"
}
```

### Response

> HTTP/1.1 200
>
> Content-Type: application/json

```JSON
{
"id": 1,
"name": "브라운",
"date": "2023-08-05",
"time": "15:40"
}
```

## 예약 취소 API

### Request

> DELETE /reservations/1 HTTP/1.1

### Response

> HTTP/1.1 200
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RoomescapeApplication {
public class RoomEscapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
SpringApplication.run(RoomEscapeApplication.class, args);
}

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

import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.format.DateTimeFormatter;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TimeFormatterConfig {
Copy link
Member

Choose a reason for hiding this comment

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

이 클래스는 무슨 역할을 하는건가요? (궁금)

Copy link
Member Author

Choose a reason for hiding this comment

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

이 클래스는 무슨 역할을 하는건가요? (궁금)

Spring Boot 에서 LocalDate, LocalTime, LocalDateTime 등의 일부 타입을 JSON 으로 바꿔주는 규칙을 이미 만들어두고 있는데, 이를 커스텀하기 위한 설정을 해주는 클래스에요. https://www.baeldung.com/spring-boot-customize-jackson-objectmapper

private static final String TIME_FORMAT = "HH:mm";

@Bean
public Jackson2ObjectMapperBuilderCustomizer localTimeSerializerCustomizer() {
return builder -> builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
}
}
Comment on lines +9 to +17
Copy link
Member

Choose a reason for hiding this comment

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

시간을 표시하는 것은 사용하는 맥락마다 달라지기 보단 공통으로 사용되는 경우가 많다고 하드라
이렇게 전역적으로 관리하는 경우가 많다고 들었으 👍
로빈의 고민이 보이는 멋진 부분이네

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

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

@Controller
@RequestMapping("/admin")
Comment on lines +7 to +8
Copy link
Member

Choose a reason for hiding this comment

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

망쵸한테도 물어봤던 부분인데 이렇게 공통되는 경로를 빼놓는 것에 대해서 어떻게 생각해?
어떤 사람은 전역적으로 검색 (cmd + shift + f)하기 어렵다고 지양하는 사람들도 있더라고

  • 전역 검색으로 /admin/reservation을 검색할 수 없음

Copy link
Member Author

Choose a reason for hiding this comment

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

public class AdminController {
@GetMapping
public String mainPage() {
return "admin/index";
}

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

import java.util.List;
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;
import roomescape.domain.Reservation;
import roomescape.dto.ReservationRequest;
import roomescape.dto.ReservationResponse;
import roomescape.repository.ReservationRepository;

@RestController
@RequestMapping("/reservations")
public class ReservationController {
private final ReservationRepository reservationRepository;

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

@PostMapping
public ReservationResponse saveReservation(@RequestBody ReservationRequest reservationRequest) {
Reservation reservation = reservationRepository.save(reservationRequest);
return toResponse(reservation);
}

private ReservationResponse toResponse(Reservation reservation) {
return new ReservationResponse(reservation.getId(),
reservation.getName(), reservation.getDate(), reservation.getTime());
}
Comment on lines +31 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.

ReservationResponse.from(reservation)과 같은 코드는 어떻게 생각!?
나는 개인적으로 위처럼 사용하는 것이 컨트롤러 코드를 깔끔히 한다고 생각
toResponse는 엄밀히 말하면 컨트롤러의 책임이 아니지 않을까 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

ReservationResponse.from(reservation)과 같은 코드는 어떻게 생각!? 나는 개인적으로 위처럼 사용하는 것이 컨트롤러 코드를 깔끔히 한다고 생각 toResponse는 엄밀히 말하면 컨트롤러의 책임이 아니지 않을까 🤔

글쎄, 컨트롤러가 자신이 응답해야 하는 형태로 데이터를 가공하는건데, Controller의 역할로 부여하는 것이 그렇게 이상한 건 아닌 것 같아.


@GetMapping
public List<ReservationResponse> findAllReservations() {
return reservationRepository.findAll()
.stream()
.map(this::toResponse)
.toList();
}

@DeleteMapping("/{reservationId}")
public void delete(@PathVariable 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.

long 사용 👍

reservationRepository.delete(reservationId);
}
}
50 changes: 50 additions & 0 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package roomescape.domain;

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

public class Reservation implements Comparable<Reservation> {
private final long id;
private final String name;
private final LocalDateTime dateTime;
Comment on lines +7 to +10
Copy link
Member

Choose a reason for hiding this comment

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

엔티티랑 도메인을 구분하지 않았군 👍
나는 엄밀하게 구분되어야 한다고 생각했는데 대부분의 경우 그렇지 않나봐 (매핑 과정만 많아지는 단점)

Copy link
Member Author

Choose a reason for hiding this comment

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

엔티티랑 도메인을 구분하지 않았군 👍 나는 엄밀하게 구분되어야 한다고 생각했는데 대부분의 경우 그렇지 않나봐 (매핑 과정만 많아지는 단점)

지금 시점에서는 이를 구분하는게 아무 의미가 없이 구조만 복잡해지는 것 같아.


public Reservation(long id, Reservation reservationBeforeSave) {
this(id, reservationBeforeSave.name, reservationBeforeSave.dateTime);
}

public Reservation(long id, String name, LocalDateTime dateTime) {
this.id = id;
this.name = name;
this.dateTime = dateTime;
}

public Reservation(long id, String name, LocalDate date, LocalTime time) {
this(id, name, LocalDateTime.of(date, time));
}

@Override
public int compareTo(Reservation other) {
return dateTime.compareTo(other.dateTime);
}

public boolean hasSameId(long id) {
return this.id == id;
}

public long getId() {
return id;
}

public String getName() {
return name;
}

public LocalDate getDate() {
return dateTime.toLocalDate();
}

public LocalTime getTime() {
return dateTime.toLocalTime();
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/dto/ReservationRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.dto;

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

public record ReservationRequest(LocalDate date, String name, LocalTime time) {
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/dto/ReservationResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.dto;

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

public record ReservationResponse(long id, String name, LocalDate date, LocalTime time) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package roomescape.repository;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.Time;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import roomescape.domain.Reservation;
import roomescape.dto.ReservationRequest;

@Repository
public class JdbcTemplateReservationRepository implements ReservationRepository {
private final JdbcTemplate jdbcTemplate;

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

@Override
public Reservation save(ReservationRequest reservationRequest) {
KeyHolder keyHolder = new GeneratedKeyHolder();
Reservation reservation = fromRequest(reservationRequest);
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(
"insert into reservation (name, date,time) values ( ?,?,? )", new String[]{"id"});
preparedStatement.setString(1, reservation.getName());
preparedStatement.setDate(2, Date.valueOf(reservation.getDate()));
preparedStatement.setTime(3, Time.valueOf(reservation.getTime()));
return preparedStatement;
}, keyHolder);
return new Reservation(keyHolder.getKey().longValue(), reservation);
Comment on lines +26 to +36
Copy link
Member

Choose a reason for hiding this comment

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

jdbcTemplate의 기술적인 한계인 것 같음
SimpleJdbcInsert라는 키워드 있던데 로빈은 이미 알고 있을 것 같지만 혹시나 해서 공유!

}

@Override
public List<Reservation> findAll() {
return jdbcTemplate.query("select * from reservation", (rs, rowNum) -> {
long id = rs.getLong("id");
String name = rs.getString("name");
LocalDate date = rs.getDate("date").toLocalDate();
LocalTime time = rs.getTime("time").toLocalTime();
return new Reservation(id, name, date, time);
});
}

@Override
public void delete(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을 사용할 필요가 없죠
long 사용 👍

jdbcTemplate.update("delete from reservation where id = ?", reservationId);
}

private Reservation fromRequest(ReservationRequest reservationRequest) {
long id = 1L;
String name = reservationRequest.name();
LocalDate date = reservationRequest.date();
Comment on lines +55 to +58
Copy link
Member

Choose a reason for hiding this comment

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

req res dto 변환 로직들을 안으로 숨길 수도 있을 것 같아요

LocalTime time = reservationRequest.time();
return new Reservation(id, name, date, time);
}
}
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 java.util.List;
import roomescape.domain.Reservation;
import roomescape.dto.ReservationRequest;

public interface ReservationRepository {
Reservation save(ReservationRequest reservationRequest);

List<Reservation> findAll();

void delete(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:product
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 IF NOT EXISTS 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)
);
7 changes: 3 additions & 4 deletions src/main/resources/templates/admin/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
Expand All @@ -15,17 +14,17 @@
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<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
6 changes: 3 additions & 3 deletions src/main/resources/templates/admin/reservation.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<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