Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
47a5417
[1 - 3단계 방탈출 예약 관리] 레디(최동근) 미션 제출합니다. (#54)
reddevilmidzy Apr 18, 2024
91636e7
chore: h2 디비 경로 추가
reddevilmidzy Apr 19, 2024
fe6c2b4
feat: 예약 테이블 스키마 생성
reddevilmidzy Apr 19, 2024
366736c
test: 데이터베이스 적용 테스트 추가
reddevilmidzy Apr 19, 2024
a2c158f
feat: 예약 저장 및 조회 기능 구현
reddevilmidzy Apr 19, 2024
401ed5e
feat: 예약 저장 기능을 컨트롤러와 연결
reddevilmidzy Apr 19, 2024
9bceeea
feat: 예약 삭제 기능 구현
reddevilmidzy Apr 19, 2024
8b4a779
style: sql 예약어 소문자에서 대문자로 변경
reddevilmidzy Apr 19, 2024
3e15676
test: 테스트 코드 리턴 타입 변경 및 상태 코드 변경
reddevilmidzy Apr 20, 2024
bae93d1
refactor: 예약 저장 및 삭제시 응답 상태코드 변경
reddevilmidzy Apr 20, 2024
296bcfc
style: 와일드카드 import 제거
reddevilmidzy Apr 20, 2024
bdb14c6
refactor: 예약 저장시 location 헤더 추가 로직 상수 사용 및 정적 팩토리 메서드 사용
reddevilmidzy Apr 20, 2024
684ba2f
fix: 예약 저장 및 삭제시 반영되지 않던 뷰 수정
reddevilmidzy Apr 20, 2024
4296b21
test: 테스트 명 변경 및 displayName 추가
reddevilmidzy Apr 20, 2024
7eb4d31
docs: api 명세 생성
reddevilmidzy Apr 20, 2024
8fbaf55
refactor: 예약 저장 시 ResponseEntity 제공 메서드 활용 및 Uri 변환 로직 변경
reddevilmidzy Apr 20, 2024
335c725
test: 예약 조회 테스트 추가
reddevilmidzy Apr 20, 2024
f9ca722
chore: test용 데이터베이스 분리
reddevilmidzy Apr 22, 2024
cde4dbd
fix: 테스트용 테이블 명 수정
reddevilmidzy Apr 22, 2024
1b67fb1
refactor: 예약 삭제 시 controller와 service의 책임 분리
reddevilmidzy Apr 22, 2024
087d49e
refactor: dao의 save 파라미터를 도메인(reservation)으로 변경
reddevilmidzy Apr 22, 2024
27818d7
refactor: controller에서 도메인 제거
reddevilmidzy Apr 22, 2024
3e1dc45
test: 불필요한 @DirtiesContext 제거
reddevilmidzy Apr 22, 2024
ec84b87
chore: 테스트시 콘솔 확인하지 못하도록 변경 및 콘솔 주소 디폴트로 설정
reddevilmidzy Apr 22, 2024
1e55564
style: 괄호 위치 컨벤션에 맞도록 변경
reddevilmidzy Apr 22, 2024
771aa3d
refactor: 단건 조회 테스트 단순화
reddevilmidzy Apr 23, 2024
521c7ee
test: 예약 조회 테스트 추가
reddevilmidzy Apr 23, 2024
00aa82a
test: db 연결 확인 테스트 추가
reddevilmidzy Apr 23, 2024
066bfe1
refactor: id를 주고 새로 Reservation 생성하는 메서드 활용
reddevilmidzy Apr 23, 2024
e15440a
refactor: 메서드 명에서 entity 제거
reddevilmidzy Apr 23, 2024
d1cae20
refactor: 파라미터에 있는 Long을 long으로 변경
reddevilmidzy Apr 23, 2024
19a70a8
refactor: 테스트 용 메서드를 제외한 기능은 하드 코딩
reddevilmidzy Apr 23, 2024
aa5e221
refactor: 테스트 용 메서드를 제외한 기능은 하드 코딩했던 것 롤백
reddevilmidzy Apr 23, 2024
92a74e6
refactor: 테스트 용 db 분리
reddevilmidzy Apr 23, 2024
7ed40ca
fix: 테스트 용 application 이름 수정
reddevilmidzy Apr 23, 2024
b69aaab
style: sql 쿼리 예약어 대문자로 변경
reddevilmidzy Apr 23, 2024
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
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 방탈출 예약 관리

## API 명세
Comment on lines +1 to +3

Choose a reason for hiding this comment

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

👍 👍 👍


### 예약 조회 API

**Request**

```http request
GET /reservations HTTP/1.1
```

<br>

**Response**

```
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 1,
"name": "브라운",
"date": "2023-01-01",
"time": "10:00"
},
{
"id": 2,
"name": "브라운",
"date": "2023-01-02",
"time": "11:00"
}
]
```
<br>

### 예약 추가 API

**Request**

```http request
POST /reservations HTTP/1.1
content-type: application/json
{
"date": "2023-08-05",
"name": "브라운",
"time": "15:40"
}
```

<br>

**Response**

```
HTTP/1.1 201
Location: reservations/{id}
Content-Type: application/json
{
"id": 1,
"name": "브라운",
"date": "2023-08-05",
"time": "15:40"
}
```

<br>

### 예약 취소 API

**Request**

```http request
DELETE /reservations/1 HTTP/1.1
```

**Response**
```
HTTP/1.1 204
```
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testRuntimeOnly 'com.h2database:h2'
Comment on lines +20 to +24

Choose a reason for hiding this comment

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

각각 라이브러리가 있는 것과 없는 것에는 어떤 차이가 있어?

}

test {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

@SpringBootApplication
public class RoomescapeApplication {
public static void main(String[] args) {

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

}
Comment on lines 6 to 12

Choose a reason for hiding this comment

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

final 광인의 면모를 다시 느끼고 갑니다.

20 changes: 20 additions & 0 deletions src/main/java/roomescape/controller/AdminController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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")
public class AdminController {

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

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

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;
import org.springframework.web.util.UriComponentsBuilder;
import roomescape.dto.ReservationRequest;
import roomescape.dto.ReservationResponse;
import roomescape.service.ReservationService;

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

private final ReservationService reservationService;

public ReservationController(final ReservationService reservationService) {
this.reservationService = reservationService;
}

@GetMapping
public List<ReservationResponse> getReservations() {
return reservationService.findAll();
}

@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)
Comment on lines +36 to +39

Choose a reason for hiding this comment

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

레디가 내 리뷰에도 남겨줬던것 같은데, 나는 오히려 해당 표현이 불필요하게 길어진다는 느낌을 받는 것 같아.
'+' 기호를 써서 URI를 만드는 것보다 위 표현이 어느 관점에서 더 좋다고 생각해?

.body(reservationResponse);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") final long id) {
try {
reservationService.remove(id);
return ResponseEntity.noContent()
.build();
} catch (final IllegalArgumentException exception) {
return ResponseEntity.notFound()
.build();
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/roomescape/dto/ReservationRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package roomescape.dto;

import roomescape.model.Reservation;

public record ReservationRequest(String name, String date, String time) {

public Reservation toReservation(final Long id) {
return new Reservation(id, name, date, time);
}
}
15 changes: 15 additions & 0 deletions src/main/java/roomescape/dto/ReservationResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package roomescape.dto;

import java.time.format.DateTimeFormatter;
import roomescape.model.Reservation;

public record ReservationResponse(Long id, String name, String date, String time) {

private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");

public static ReservationResponse from(final Reservation reservation) {
final String date = reservation.getDate().format(DateTimeFormatter.ISO_DATE);
final String time = reservation.getTime().format(TIME_FORMATTER);
return new ReservationResponse(reservation.getId(), reservation.getName(), date, time);
}
Comment on lines +8 to +14

Choose a reason for hiding this comment

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

약간 억지(?)일수도 있다고 생각하는데, ISO_DATE 보다는 직접 글자로 명시해주는게 더 직관적일수 있다 생각하는데, 레디는 어떻게 생각해?

Suggested change
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
public static ReservationResponse from(final Reservation reservation) {
final String date = reservation.getDate().format(DateTimeFormatter.ISO_DATE);
final String time = reservation.getTime().format(TIME_FORMATTER);
return new ReservationResponse(reservation.getId(), reservation.getName(), date, time);
}
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
public static ReservationResponse from(final Reservation reservation) {
final String date = reservation.getDate().format(DATE_FORMATTER);
final String time = reservation.getTime().format(TIME_FORMATTER);
return new ReservationResponse(reservation.getId(), reservation.getName(), date, time);
}

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

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Objects;

public class Reservation {

private final Long id;
private final String name;
private final LocalDate date;
private final LocalTime time;

private Reservation(final Long id, final String name, final LocalDate date, final LocalTime time) {
this.id = id;
this.name = name;
this.date = date;
this.time = time;
}

public Reservation(final Long id, final String name, final String date, final String time) {
this(id, name, LocalDate.parse(date), LocalTime.parse(time));
}

public static Reservation create(final String name, final String date, final String time) {
return new Reservation(null, name, date, time);
}
Comment on lines +25 to +27
Copy link

Choose a reason for hiding this comment

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

생성자가 아닌 정적 팩토리 메서드를 만든 이유가 있을까? 그리고 메서드 명을 create로 한 것도 이유가 궁금해!


public Reservation toReservation(final long id) {
return new Reservation(id, name, date, time);
}
Comment on lines +29 to +31
Copy link

Choose a reason for hiding this comment

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

개인적으로 이 메서드명도 의미가 명확하지 않다고 느껴지는 것 같아!
예를 들어, reservation.toReservation(id) 이런 식으로 코드를 작성할 것 같은데, 이 문장 자체는 이미 존재하는 reservation을 toReservation한다? 예약을 예약으로 변경하는 것으로 느껴져서 어색하다고 생각해


public Long getId() {
return id;
}

public String getName() {
return name;
}

public LocalDate getDate() {
return date;
}

public LocalTime getTime() {
return time;
}

@Override
public boolean equals(final Object target) {
if (this == target) {
return true;
}
if (target == null || getClass() != target.getClass()) {
return false;
}
final Reservation reservation = (Reservation) target;
return Objects.equals(getId(), reservation.getId()) && Objects.equals(getName(), reservation.getName())
&& Objects.equals(getDate(), reservation.getDate()) && Objects.equals(getTime(),
reservation.getTime());
}

@Override
public int hashCode() {
return Objects.hash(getId(), getName(), getDate(), getTime());
}
Comment on lines +49 to +66
Copy link

Choose a reason for hiding this comment

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

적절한 동등성 부여인지 고민이 필요할 것 같아! id만 같으면 동등한 객체로 보아도 괜찮지 않을까?


@Override
public String toString() {
return "Reservation{" +
"id=" + id +
", name='" + name + '\'' +
", date=" + date +
", time=" + time +
'}';
}
}
73 changes: 73 additions & 0 deletions src/main/java/roomescape/repository/ReservationDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package roomescape.repository;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.Time;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.sql.DataSource;
import org.springframework.dao.EmptyResultDataAccessException;
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.model.Reservation;

@Repository
public class ReservationDao {

private final JdbcTemplate jdbcTemplate;

public ReservationDao(final DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

public Reservation save(final Reservation reservation) {
final String sql = "INSERT INTO reservation (name, date, time) VALUES (?, ?, ?)";
final KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(connection -> {
final PreparedStatement preparedStatement = connection.prepareStatement(sql, 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);

final long id = Objects.requireNonNull(keyHolder.getKey()).longValue();
return reservation.toReservation(id);
}

public Optional<Reservation> findById(final long id) {
try {
final Reservation reservation = jdbcTemplate.queryForObject(
"SELECT id, name, date, time FROM reservation WHERE id = ?",
(resultSet, rowNum) -> new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getString("time")
), id);
return Optional.ofNullable(reservation);
} catch (final EmptyResultDataAccessException exception) {
return Optional.empty();
}
}
Comment on lines +52 to +56
Copy link

Choose a reason for hiding this comment

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

EmptyResultDataAccessException 만 예외 처리한 이유가 무엇인지 궁금해! 그리고 EmptyResultDataAccessException 가 발생할 수 있다는 것을 어떻게 알았는지도 궁금해


public List<Reservation> findAll() {
return jdbcTemplate.query(
"SELECT id, name, date, time FROM reservation",
(resultSet, rowNum) -> new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("date"),
resultSet.getString("time")
));
}

public void remove(final long id) {
final String sql = "DELETE FROM reservation WHERE id = ?";
jdbcTemplate.update(sql, id);
}
}
Loading