Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5277a36
feat: Spring Web, Thymeleaf 의존성 추가
gjtjrl303 Apr 30, 2025
3ae6863
docs: README 작성
gjtjrl303 Apr 30, 2025
6e05245
feat: 2단계 테스트 코드 작성
gjtjrl303 Apr 30, 2025
363baa0
feat: homeController 구현
gjtjrl303 Apr 30, 2025
0a84565
feat: Reservation 객체 구현
gjtjrl303 Apr 30, 2025
0c7afc6
feat: Reservation dto 객체 구현
gjtjrl303 Apr 30, 2025
2f99f81
feat: ReservationController 구현
gjtjrl303 Apr 30, 2025
4ceb2a8
refactor: @RequestMapping → @GetMapping으로 변경 (/ 경로)변경
gjtjrl303 May 3, 2025
698ae7e
feat: MemoryReservationRepository 구현
gjtjrl303 May 3, 2025
e09efa9
refactor: Reservation 클래스의 필드 final 제거하여 유연한 값 변경 가능하게 수정
gjtjrl303 May 3, 2025
e597d1b
refactor: controller에 있던 예약 저장 로직을 repository로 이동
gjtjrl303 May 3, 2025
d11c3a4
refactor: ReservationResponse DTO를 class에서 record로 변경
gjtjrl303 May 3, 2025
6f40799
feat: ReservationService 구현
gjtjrl303 May 7, 2025
16b13ed
feat: ReservationRequestValidator 구현
gjtjrl303 May 7, 2025
9203e52
refactor: API 전용 컨트롤러와 페이지 컨트롤러 분리
gjtjrl303 May 7, 2025
34444aa
feat: NotFoundReservationException 구현
gjtjrl303 May 7, 2025
a0685fd
feat: test코드 구현
gjtjrl303 May 7, 2025
a10ee63
docs: README 작성
gjtjrl303 May 7, 2025
f5d9211
feat: ErrorResult 객체 구현
gjtjrl303 May 7, 2025
c670ca7
refactor: Reservation 기본 생성자 삭제
gjtjrl303 May 7, 2025
958dd27
delete: ReservationController 삭제
gjtjrl303 May 7, 2025
ca8c4f1
feat: ReservationExceptionHandler 구현
gjtjrl303 May 7, 2025
6f5afca
feat: ReservationRequest dto 구현
gjtjrl303 May 7, 2025
984c1fa
feat: 충돌 해결
gjtjrl303 May 7, 2025
b98bf11
feat: 충돌 해결
gjtjrl303 May 7, 2025
99190e7
refactor: 사용하지 않는 코드 제거
gjtjrl303 May 7, 2025
6dd45ef
docs: 도메인 특화 용어 제거
gjtjrl303 May 10, 2025
809e2db
refactor: ErrorResult class에서 record로 변경
gjtjrl303 May 10, 2025
a3d38dd
feat: GlobalExceptionHandler를 통해 전역 및 예상치 못한 예외 처리 추가
gjtjrl303 May 10, 2025
8e49f4e
refactor: 테스트 코드에서 과거 날짜 → 현재 이후 날짜로 수정
gjtjrl303 May 10, 2025
5a9fc02
feat: Reservation 관련 커스텀 예외 구현
gjtjrl303 May 10, 2025
6e028d7
refactor: dto 패키지 위치 controller로 이동
gjtjrl303 May 10, 2025
ef07788
refactor: ExceptionHandler GlobalExceptionHandler로 통합
gjtjrl303 May 10, 2025
967b876
refactor: Reservation 검증 로직 entity내부로 이동
gjtjrl303 May 10, 2025
ac439b1
feat: ReservationDTO 변환을 위한 mapper 구현
gjtjrl303 May 10, 2025
7af7815
refactor: mapper를 통해 DTO 변환하도록 변경
gjtjrl303 May 10, 2025
05e844e
feat: 서비스 Reservation 반환 DTO 구현
gjtjrl303 May 10, 2025
fa64bd3
refactor: 존재하는 id 추가할때 커스텀 예외를 던지도록 변경
gjtjrl303 May 10, 2025
e5e2d78
feat: 서비스 save메서드를 위한 dto 구현
gjtjrl303 May 10, 2025
d0f88e9
delete: ReservationMapper 삭제
gjtjrl303 May 11, 2025
7001478
feat: Reservation 엔티티에 필드 검증 로직 추가
gjtjrl303 May 11, 2025
cd2ce20
refactor: ReservationRequest의 검증 로직을 메서드로 분리
gjtjrl303 May 11, 2025
660d181
refactor: DTO 변환을 Mapper에서 객체 내부 메서드로 이동
gjtjrl303 May 11, 2025
59454c7
refactor: 일관된 예외 메시지 대신 e.getMessage()로 상세 원인 제공
gjtjrl303 May 11, 2025
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
141 changes: 141 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,144 @@
- /reservation: 예약 관리 페이지 (reservation.html) 응답
- /reservations: 예약 데이터 JSON 응답

## 3단계 - 예약 등록
- `POST /reservations`: 새로운 예약 정보를 등록합니다.
- 요청 본문에 예약자의 이름, 날짜, 시간이 포함됩니다.
- 성공 시 `201 Created` 응답 및 Location 헤더 반환

## 4단계 - 예외 처리
- 클라이언트 요청의 문제로 예외가 발생할 경우 `400 Bad Request`를 반환합니다.
- 처리되는 예외 예시:
- 예약 입력값 누락 (name, date, time 중 하나라도 누락)
- 존재하지 않는 예약 ID로 삭제 요청
- JSON 파싱 오류 (날짜/시간 형식이 올바르지 않음)

# 📌 API 명세서

---

## GET /reservations

- 설명: 모든 예약 목록 조회
- 응답: `200 OK`
- 응답 본문 예시:

```json
[
{
"id": 1,
"name": "브라운",
"date": "2023-08-05",
"time": "15:40"
},
{
"id": 2,
"name": "죠르디",
"date": "2023-08-05",
"time": "17:00"
}
// ... 더 많은 예약
]

```
| 필드 | 타입 | 설명 |
| ------ | --------------------- | ------ |
| `id` | `number` | 예약 식별자 |
| `name` | `string` | 예약자 이름 |
| `date` | `string (YYYY-MM-DD)` | 예약 날짜 |
| `time` | `string (HH:mm)` | 예약 시간 |

---
## POST /reservations
- 설명: 예약 추가
- 응답: `201 Created`
- 응답 본문 예시:
``` json
{
"id": 1,
"name": "브라운",
"date": "2023-08-05",
"time": "15:40"
}
```
| 필드 | 타입 | 필수 여부 | 설명 |
| ------ | --------------------- | ----- | ------ |
| `name` | `string` | ✅ | 예약자 이름 |
| `date` | `string (YYYY-MM-DD)` | ✅ | 예약 날짜 |
| `time` | `string (HH:mm)` | ✅ | 예약 시간 |
---
## DELETE /reservations/{id}
- 설명: 예약 삭제
- 응답: `204 No Content`
- 응답 본문 예시: 없음

---

## ❗ 예외 응답 명세

### 잘못된 요청 (필드 누락 등)

- 상태 코드: `400 Bad Request`
- 발생 조건:
- 예약 요청에서 name, date, time 중 하나라도 누락되거나
- 빈 문자열 또는 null 값이 들어온 경우

응답 예시:
```json
{
"code": "BAD_REQUEST",
"message": "이름은 필수입니다."
}
```
### 존재하지 않는 예약 ID
- 상태 코드: `400 Bad Request`
- 발생 조건: 존재하지 않는 ID를 가진 예약을 삭제할 때

응답 예시:
```json
{
"code": "NOT_FOUND_RESERVATION",
"message": "해당 예약이 존재하지 않습니다."
}
```

### JSON 파싱 오류
- 상태 코드: `400 Bad Request`
- 발생 조건: 요청 JSON이 잘못된 형식일 경우

응답 예시:
```json
{
"code": "INVALID_JSON",
"message": "형식이 옳바르지 않습니다"
}
```

### 존재하는 ID 중복 예약
- 상태 코드: `409 CONFLICT`
- 발생 조건: 중복된 ID의 Reservation을 예약할 경우

응답 예시:
```json
{
"code": "RESERVATION_ALREADY_EXISTS",
"message": "이미 ID가 존재하는 예약은 저장할 수 없습니다.
}
```

### 예상하지 못한 오류
- 상태 코드: `500 Internal Server Error`
- 발생 조건: 서버 내부에서 예상하지 못한 예외가 발생한 경우

응답 예시:
```json
{
"code": "INTERNAL_SERVER_ERROR",
"message": "예상치 못한 오류 발생"
}
```





Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
public class PageController {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

정적 페이지를 제공하는 역할에 맞는 더 적합한 네이밍이네요.
섬세하시군요!👍


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

@GetMapping("/reservation")
public String reservation() {
return "reservation";
}
}

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

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import roomescape.controller.dto.ReservationRequest;
import roomescape.controller.dto.ReservationResponse;
import roomescape.mapper.ReservationMapper;
import roomescape.service.ReservationService;
import roomescape.service.dto.ReservationResult;
import roomescape.service.dto.SaveReservationCommand;

import java.net.URI;
import java.util.List;

@RestController
public class ReservationApiController {

private final ReservationService reservationService;
private final ReservationMapper mapper;

public ReservationApiController(ReservationService reservationService, ReservationMapper mapper) {
this.reservationService = reservationService;
this.mapper = mapper;
}

@GetMapping("/reservations")
public ResponseEntity<List<ReservationResponse>> findAll() {
List<ReservationResult> all = reservationService.findAll();
List<ReservationResponse> reservationResponses = all.stream()
.map(mapper::ResultToResponse)
.toList();
return ResponseEntity.ok(reservationResponses);
}

@PostMapping("/reservations")
public ResponseEntity<ReservationResponse> save(@RequestBody ReservationRequest reservationRequest) {
SaveReservationCommand command = mapper.requestToCommand(reservationRequest);
ReservationResult result = reservationService.save(command);
ReservationResponse reservationResponse = mapper.ResultToResponse(result);
return ResponseEntity.created(URI.create("/reservations/" + result.id()))
.contentType(MediaType.APPLICATION_JSON)
.body(reservationResponse);
}

@DeleteMapping("/reservations/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
reservationService.delete(id);
return ResponseEntity.noContent().build();
}
}
43 changes: 0 additions & 43 deletions src/main/java/roomescape/controller/ReservationController.java

This file was deleted.

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

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

public class ReservationRequest {
private final String name;
private final LocalDate date;
private final LocalTime time;

public ReservationRequest(String name, LocalDate date, LocalTime time) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 필수입니다.");
}
if (date == null) {
throw new IllegalArgumentException("날짜는 필수입니다.");
}
if (time == null) {
throw new IllegalArgumentException("시간은 필수입니다.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Controller로 들어오는 요청 데이터를 검증 하는 로직이 추가 되었네요.

간단한 퀴즈 하나 내보겠습니다..!

{
    "date": "2025-08-05",
    "name": null,
    "time": "15:40"
}

이런 요청이 들어 왔을 때, ExceptionHandler에서는 어떤 예외를 catch해서 처리할까요?
한번 예외 케이스의 요청으로 보내고 응답하는 예외 메세지를 확인 해보세요!


기본적으로 jackson 라이브러리를 통해서 json 형태의 텍스트가 저희가 정의한 객체로 만들어지게 됩니다.
해당 과정에서 석준님이 정의한 예외(IllegalArgumentException)이 전환 되는 것 같아요.

퀴즈와는 조금 먼 내용이지만,
json 요청을 기반으로 객체가 생성 되는 과정 혹은 방식을 살펴보는 것도 좋은 지식이 될 수 있어요.

참고 하면 좋을 자료

@gjtjrl303 gjtjrl303 May 11, 2025 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

스크린샷 2025-05-11 오후 2 23 05

IllegalArgumentException으로 처리될 줄 알았는데, 실제로는 HttpMessageNotReadableException으로 처리되더라고요.
왜 그런지 궁금해서 디버깅을 해보니, AbstractJackson2HttpMessageConverter 내부에서
IllegalArgumentException을 감싸 HttpMessageNotReadableException으로 변환해 던지네요.

현재는 HttpMessageNotReadableException이 발생할 경우
"형식이 옳바르지 않습니다"라는 고정된 메시지를 반환하고 있는데,
실제 예외의 원인을 확인할 수 있도록 e.getMessage()를 그대로 활용하는 것이 더 좋은 방법 같아요.

나중에 로그를 남길 때도 이 부분에 대해서 생각을 하며 로그를 남기면
문제 원인을 파악하는 데 훨씬 도움이 되겠다는 생각이 드네요. 😊

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

꼼꼼하게 트래킹 해보셨군요..!👍👍
원인 분석까지 잘 파악하셨네요.

이처럼 의도치 않게 작동하는 로직이 존재 할 수 있으니, 추후 테스트를 더 보강 하여 이런 문제를 조기에 발견 할 수 있음 좋겠네요ㅎㅎ


this.name = name;
this.date = date;
this.time = time;
}

public String getName() {
return name;
}

public LocalDate getDate() {
return date;
}

public LocalTime getTime() {
return time;
}
}

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

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

public record ReservationResponse(
Long id,
String name,
LocalDate date,
LocalTime time
) {
}
8 changes: 7 additions & 1 deletion src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ public Reservation(Long id, String name, LocalDate date, LocalTime time) {
}

public Reservation(String name, LocalDate date, LocalTime time) {
validateDate(date);
this.id = null;
this.name = name;
this.date = date;
this.time = time;
}
Comment on lines 20 to 25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

생성 단계에서 검증이 이루어지네요.

현재 Reservation에는 2개의 생성자가 있는데...
날짜에 대한 정책은 해당 생성자를 통해서 만드는 경우에만 지켜질 것 같아요.

validateDate()를 주 생성자로 옮기고 this()를 통해 재사용 할 수도 있어요.

Suggested change
public Reservation(String name, LocalDate date, LocalTime time) {
validateDate(date);
this.id = null;
this.name = name;
this.date = date;
this.time = time;
}
// 주 생성자
public Reservation(Long id, String name, LocalDate date, LocalTime time) {
validateDate(date);
this.id = id;
this.name = name;
this.date = date;
this.time = time;
}
// 부 생성자
public Reservation(String name, LocalDate date, LocalTime time) {
this(null, name, date, time);
}


public Reservation withId(Long id) {
return new Reservation(id, this.name, this.date, this.time);
}
Expand All @@ -43,4 +43,10 @@ public LocalDate getDate() {
public LocalTime getTime() {
return time;
}

private void validateDate(LocalDate date) {
if (date.isBefore(LocalDate.now())) {
throw new IllegalArgumentException("예약 날짜는 오늘 이후여야 합니다.");
}
}
}
22 changes: 0 additions & 22 deletions src/main/java/roomescape/dto/ReservationResponse.java

This file was deleted.

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

public record ErrorResult(
String code,
String message
) {}
Loading