Skip to content

Commit 3ef700a

Browse files
authored
고생하셨습니다.
🎉 PR 머지 완료! 🎉
1 parent e7e5e5d commit 3ef700a

12 files changed

+541
-0
lines changed

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# 📘 Reservation API 명세서
2+
3+
예약 생성, 조회, 삭제 기능이 있으며
4+
잘못된 요청 발생 시 400 BadRequest로 응답합니다.
5+
6+
---
7+
8+
## 1.예약자 조회
9+
10+
**GET** `/reservations`
11+
12+
### ✅ Response
13+
14+
- **200 OK**
15+
- **Body**
16+
```json
17+
[
18+
{
19+
"id": 1,
20+
"name": "브라운",
21+
"date": "2023-08-05",
22+
"time": "15:40"
23+
}
24+
]
25+
```
26+
27+
---
28+
29+
## 2. 예약 추가
30+
31+
**POST** `/reservations`
32+
33+
### ✅ Request
34+
```http
35+
Content-Type: application/json
36+
```
37+
38+
```json
39+
{
40+
"name": "브라운",
41+
"date": "2023-08-05",
42+
"time": "15:40"
43+
}
44+
```
45+
46+
### ✅ Response
47+
48+
- **201 Created**
49+
- **Location Header**: `/reservations/1`
50+
- **Body**
51+
```json
52+
{
53+
"id": 1,
54+
"name": "브라운",
55+
"date": "2023-08-05",
56+
"time": "15:40"
57+
}
58+
```
59+
60+
### ❌ 예외 상황 (400 Bad Request)
61+
62+
| 조건 | 설명 |
63+
|----------------------------------|-----------------------------|
64+
| name이 null 또는 공백 | 예약자 이름 누락 |
65+
| date 또는 time이 null | 날짜/시간 누락 |
66+
| 날짜가 과거일 경우 | 과거 예약 불가 |
67+
| 동일한 name+date+time 조합 존재 | 중복 예약 금지 |
68+
---
69+
70+
## 🔹 3. 예약 삭제
71+
72+
**DELETE** `/reservations/{id}`
73+
74+
### ✅ Path Parameter
75+
76+
- `id` : 예약 ID (정수)
77+
78+
### ✅ Response
79+
80+
- **204 No Content**
81+
82+
### ❌ 예외 상황 (400 Bad Request)
83+
84+
| 조건 | 설명 |
85+
|-----------------------|----------------------------|
86+
| 존재하지 않는 id 삭제 | 해당 ID의 예약이 없음 |
87+
| id가 0 이하인 경우 | 유효하지 않은 식별자 |
88+
89+
---
90+
91+
## 🔧 에러 응답 예시
92+
93+
```http
94+
HTTP/1.1 400 Bad Request
95+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package roomescape.controller;
2+
3+
import org.springframework.stereotype.Controller;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
6+
@Controller
7+
public class PageController {
8+
9+
@GetMapping("/")
10+
public String home() {
11+
return "home";
12+
}
13+
14+
@GetMapping("/reservation")
15+
public String reservationPage() {
16+
return "reservation";
17+
}
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package roomescape.controller;
2+
3+
import java.net.URI;
4+
import java.util.List;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.DeleteMapping;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.PostMapping;
11+
import org.springframework.web.bind.annotation.RequestBody;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.ResponseStatus;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import roomescape.domain.Reservation;
16+
import roomescape.dto.ReservationResponse;
17+
import roomescape.service.ReservationService;
18+
19+
@RestController
20+
@RequestMapping("/reservations")
21+
public class ReservationController {
22+
23+
private final ReservationService reservationService;
24+
25+
public ReservationController(ReservationService reservationService) {
26+
this.reservationService = reservationService;
27+
}
28+
29+
@PostMapping
30+
public ResponseEntity<ReservationResponse> add(@RequestBody Reservation reservation) {
31+
Reservation saved = reservationService.add(reservation);
32+
return ResponseEntity.created(URI.create("/reservations/" + saved.getId()))
33+
.body(new ReservationResponse(saved));
34+
}
35+
36+
@GetMapping
37+
public List<ReservationResponse> findAll() {
38+
return reservationService.findAll().stream()
39+
.map(ReservationResponse::new)
40+
.toList();
41+
}
42+
43+
@DeleteMapping("/{id}")
44+
@ResponseStatus(HttpStatus.NO_CONTENT)
45+
public void delete(@PathVariable int id) {
46+
reservationService.delete(id);
47+
}
48+
49+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package roomescape.domain;
2+
3+
import java.time.LocalDate;
4+
import java.time.LocalTime;
5+
import java.util.Objects;
6+
import roomescape.exception.InvalidReservationException;
7+
8+
public class Reservation {
9+
10+
private final Integer id;
11+
private final String name;
12+
private final LocalDate date;
13+
private final LocalTime time;
14+
15+
public Reservation(Integer id, String name, LocalDate date, LocalTime time) {
16+
validate(name, date, time);
17+
this.id = id;
18+
this.name = name;
19+
this.date = date;
20+
this.time = time;
21+
}
22+
23+
public Reservation() {
24+
this.id = null;
25+
this.name = null;
26+
this.date = null;
27+
this.time = null;
28+
}
29+
30+
private void validate(String name, LocalDate date, LocalTime time) {
31+
if (name == null || name.trim().isEmpty()) {
32+
throw new InvalidReservationException("이름은 필수입니다.");
33+
}
34+
if (date == null || time == null) {
35+
throw new InvalidReservationException("날짜와 시간은 필수입니다.");
36+
}
37+
if (date.isBefore(LocalDate.now())) {
38+
throw new InvalidReservationException("과거 날짜로 예약할 수 없습니다.");
39+
}
40+
}
41+
42+
public Integer getId() {
43+
return id;
44+
}
45+
46+
public String getName() {
47+
return name;
48+
}
49+
50+
public LocalDate getDate() {
51+
return date;
52+
}
53+
54+
public LocalTime getTime() {
55+
return time;
56+
}
57+
58+
@Override
59+
public boolean equals(Object o) {
60+
if (this == o) return true;
61+
if (o == null || getClass() != o.getClass()) return false;
62+
Reservation that = (Reservation) o;
63+
return Objects.equals(id, that.id);
64+
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hash(id);
69+
}
70+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package roomescape.dto;
2+
3+
import roomescape.domain.Reservation;
4+
5+
public class ReservationResponse {
6+
7+
private final int id;
8+
private final String name;
9+
private final String date;
10+
private final String time;
11+
12+
public ReservationResponse(Reservation reservation) {
13+
this.id = reservation.getId();
14+
this.name = reservation.getName();
15+
this.date = reservation.getDate().toString();
16+
this.time = reservation.getTime().toString();
17+
}
18+
19+
public int getId() {
20+
return id;
21+
}
22+
23+
public String getName() {
24+
return name;
25+
}
26+
27+
public String getDate() {
28+
return date;
29+
}
30+
31+
public String getTime() {
32+
return time;
33+
}
34+
}
35+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package roomescape.exception;
2+
3+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.http.converter.HttpMessageNotReadableException;
9+
import org.springframework.web.bind.annotation.ExceptionHandler;
10+
import org.springframework.web.bind.annotation.RestControllerAdvice;
11+
12+
@RestControllerAdvice
13+
public class GlobalExceptionHandler {
14+
15+
@ExceptionHandler(HttpMessageNotReadableException.class)
16+
public ResponseEntity<Map<String, String>> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
17+
Map<String, String> error = new HashMap<>();
18+
19+
if (e.getCause() instanceof InvalidFormatException) {
20+
error.put("error", "InvalidFormat Exception : 입력 형식이 올바르지 않습니다.");
21+
} else {
22+
error.put("error", "HTTP Not Readable Excetption : 요청 형식이 올바르지 않습니다.");
23+
}
24+
25+
return ResponseEntity.badRequest().body(error);
26+
}
27+
28+
@ExceptionHandler(InvalidReservationException.class)
29+
public ResponseEntity<Map<String, String>> handleInvalidReservation(InvalidReservationException e) {
30+
Map<String, String> error = new HashMap<>();
31+
error.put("error", e.getMessage());
32+
return ResponseEntity.badRequest().body(error);
33+
}
34+
35+
@ExceptionHandler(NotFoundReservationException.class)
36+
public ResponseEntity<Map<String, String>> handleNotFoundReservation(NotFoundReservationException e) {
37+
Map<String, String> error = new HashMap<>();
38+
error.put("error", e.getMessage());
39+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
40+
}
41+
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package roomescape.exception;
2+
3+
public class InvalidReservationException extends RuntimeException {
4+
public InvalidReservationException(String message) {
5+
super(message);
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package roomescape.exception;
2+
3+
public class NotFoundReservationException extends RuntimeException {
4+
public NotFoundReservationException(String message) {
5+
super(message);
6+
}
7+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package roomescape.service;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.concurrent.atomic.AtomicLong;
6+
import org.springframework.stereotype.Service;
7+
import roomescape.domain.Reservation;
8+
import roomescape.exception.InvalidReservationException;
9+
import roomescape.exception.NotFoundReservationException;
10+
11+
@Service
12+
public class ReservationService {
13+
14+
private final List<Reservation> reservations = new ArrayList<>();
15+
private final AtomicLong index = new AtomicLong(1);
16+
17+
public Reservation add(Reservation reservation) {
18+
Reservation newReservation = new Reservation(
19+
(int) index.getAndIncrement(),
20+
reservation.getName(),
21+
reservation.getDate(),
22+
reservation.getTime()
23+
);
24+
25+
boolean isDuplicate = reservations.stream().anyMatch(r ->
26+
r.getName().equals(reservation.getName()) &&
27+
r.getDate().equals(reservation.getDate()) &&
28+
r.getTime().equals(reservation.getTime())
29+
);
30+
if (isDuplicate) {
31+
throw new InvalidReservationException("동일한 예약이 이미 존재합니다.");
32+
}
33+
34+
reservations.add(newReservation);
35+
return newReservation;
36+
}
37+
38+
public List<Reservation> findAll() {
39+
return reservations;
40+
}
41+
42+
public void delete(int id) {
43+
boolean removed = reservations.removeIf(r -> r.getId() == id);
44+
if (!removed) {
45+
throw new NotFoundReservationException("해당 ID가 없습니다.");
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)