Skip to content
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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}

test {
Expand Down
69 changes: 69 additions & 0 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package roomescape.controller;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import roomescape.dto.ReservationAddReq;
import roomescape.dto.ReservationReq;
import roomescape.exception.InvalidRequestReservationException;
import roomescape.exception.NotFoundReservationException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

@Controller
public class ReservationController {

//AtomicLong: Long 자료형 가지는 Wrapping 클래스. incrementAndGet(): ++x
private AtomicLong index = new AtomicLong(0);
private List<ReservationReq> reservations = new ArrayList<>();

@GetMapping("/reservation")
public String reservation(Model model){

model.addAttribute(reservations);
return "reservation";
}

//예약 조회
@GetMapping("/reservations")
@ResponseBody
public List<ReservationReq> reservations(){

return reservations;
}

//예약 추가
@PostMapping("/reservations")
@ResponseBody
@ResponseStatus(HttpStatus.CREATED)
public ReservationReq addReservation(@RequestBody ReservationAddReq reservation, HttpServletResponse response){
if(reservation.getName().isEmpty()||reservation.getDate().isEmpty()||reservation.getTime().isEmpty()){
Copy link

@juanxiu juanxiu Nov 23, 2025

Choose a reason for hiding this comment

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

파라미터 입력을 어떻게 체크하면 좋을까요? Controller에 Valid 어노테이션을 추가하면 수동으로 검증 코드를 작성하지 않아도 됩니다. DTO 에 정의된 필드가 Controller 에서 ResquestBody 에 들어올 때 검증해주어요. 자세한 내용은 이 글을 참고해볼까요? 그리고 이 어노테이션은 검증 실패 시 어떤 에러를 반환하는지 알아보고 리팩터링 해볼까요?

throw new InvalidRequestReservationException("필요한 인자가 없습니다.");
}
ReservationReq newReservation =new ReservationReq(index.incrementAndGet(),reservation.getName(),reservation.getDate(),reservation.getTime());
reservations.add(newReservation);

// ResponseEntity 사용하면 header 명시적으로 지정하지 않아도 된다고 한다.
Copy link

Choose a reason for hiding this comment

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

@ResponseStatus 를 사용하고 있군요. ResponseEntity 와 차이점을 생각해볼까요? 리턴값을 일관된 구조로 클라에게 전달하기 위해 둘 중 어느 것을 사용해야 할까요?

이 글 을 참고해보면 좋을 것 같아요!

response.setHeader("Location", "/reservations/" + newReservation.getId());
return newReservation;
}

//예약 삭제
@DeleteMapping("/reservations/{id}")
@ResponseBody
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteReservation(@PathVariable Long id){
ReservationReq delete=reservations.stream().filter(ReservationReq -> id.equals(ReservationReq.getId())).findAny().orElse(null);
if(delete==null){
throw new NotFoundReservationException("삭제할 예약이 없습니다");
}
reservations.remove(delete);
}


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

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

@Controller
public class StartController {

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




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

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ReservationAddReq {
private String name;
private String date; //우선 String으로.. 차후 Date로 형변환 필요하면 교체..
private String time; //동일!!
}
17 changes: 17 additions & 0 deletions src/main/java/roomescape/dto/ReservationReq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package roomescape.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor //기본 생성자 자동 생성
public class ReservationReq {
//캡슐화를 위해서는 private으로 해야 하지만, step12의 코드에서 private으로 하면 JSON이 접근을 못 해서 직렬화 못 하는 에러 발생
//해결을 위해 lombok의 getter 추가
private Long id;
private String name;
private String date; //우선 String으로.. 차후 Date로 형변환 필요하면 교체..
private String time; //동일!!


}
21 changes: 21 additions & 0 deletions src/main/java/roomescape/exception/ExceptionHandlers.java
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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;


@ControllerAdvice
public class ExceptionHandlers {

@ExceptionHandler(NotFoundReservationException.class)
public ResponseEntity NotFoundReservationException() { //딱히 메시지에 id 등 추가할 필요를 못 느껴서 매개변수 없앰.
Copy link

Choose a reason for hiding this comment

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

메서드명과 예외클래스명을 일치시키면 혼동이 발생할 수 있어요. 메서드명을 변경해볼까요?

return ResponseEntity.badRequest().build();
Copy link

Choose a reason for hiding this comment

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

현재는 상태코드만 리턴하고 있네요. 예외처리를 하는 이유는 클라에게 오류 정보를 전달하기 위해서예요. 오류 메시지를 담는 적절한 객체를 만들어 전달하면 더 좋을 것 같네요!

Copy link

Choose a reason for hiding this comment

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

NotFound 예외는 404 상태코드로 전달하는 게 좋을 것 같아요! 현재는 Bad request 400 코드로 처리하고 있네요!

}

@ExceptionHandler(InvalidRequestReservationException.class)
public ResponseEntity InvalidRequestReservationException(){
return ResponseEntity.badRequest().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class InvalidRequestReservationException extends RuntimeException {
public InvalidRequestReservationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class NotFoundReservationException extends RuntimeException {
public NotFoundReservationException(String message) {
super(message);
}
}
76 changes: 76 additions & 0 deletions src/test/java/roomescape/MissionStepTest.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package roomescape;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.Matchers.is;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class MissionStepTest {
Expand All @@ -16,4 +22,74 @@ public class MissionStepTest {
.then().log().all()
.statusCode(200);
}

@Test
void 이단계() {
RestAssured.given().log().all()
.when().get("/reservation")
.then().log().all()
.statusCode(200);

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(0)); // 아직 생성 요청이 없으니 Controller에서 임의로 넣어준 Reservation 갯수(=1) 만큼 검증.
}

@Test
void 삼단계() {
Map<String, String> params = new HashMap<>();
params.put("name", "브라운");
params.put("date", "2023-08-05");
params.put("time", "15:40");

RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(params)
.when().post("/reservations")
.then().log().all()
.statusCode(201)
.header("Location", "/reservations/1")
.body("id", is(1));

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(1));

RestAssured.given().log().all()
.when().delete("/reservations/1")
.then().log().all()
.statusCode(204);

RestAssured.given().log().all()
.when().get("/reservations")
.then().log().all()
.statusCode(200)
.body("size()", is(0));
}

@Test
void 사단계() {
Map<String, String> params = new HashMap<>();
params.put("name", "브라운");
params.put("date", "");
params.put("time", "");

// 필요한 인자가 없는 경우
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(params)
.when().post("/reservations")
.then().log().all()
.statusCode(400);

// 삭제할 예약이 없는 경우
RestAssured.given().log().all()
.when().delete("/reservations/1")
.then().log().all()
.statusCode(400);
}
}