diff --git a/README.md b/README.md
new file mode 100644
index 000000000..8a7567008
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+# 간단 스프링 어플리케이션
+
+## Core
+### 8단계
+- [x] 시간 테이블 설정
+- [x] 시간 조회, 추가, 삭제 기능 추가
+- [x] DB 초기 값 설정
+### 9단계
+- [x] 해당 시간이 존재하는 예약이 남아있을 때, 시간 삭제 못하도록 예외처리
+- [x] 중복된 시간(시간의 time값이 같은) DB에 저장하지 못하도록 예외처리
+### 10단계
+
+### 고민사항
+- 프로젝트 구조:
+[도메인 우선 vs 레이어 우선](https://codewithandrea.com/articles/flutter-project-structure/)
+
+우선 프로젝트 구조를 잘 가져가면 가져올 수 있는 효과가 무엇이 있는지 궁금합니다
+미션처럼 크기가 작은 프로젝트에서는 레이어 구조 또한 괜찮은 방향 같지만 서비스 크기가
+큰 앱을 개발하는데 있어서는 도메인 단위로 나누는 것이 프로젝트으 복잡성을 줄일 수 있게 되는거 같습니다.
+그러나 도메인 단위로 나뉘게 된다면 중복된 Entity(테이블이 서로 연관되어 있을 가능성이 크기 )에 대한 관리를 어떻게 하는지 궁금합니다!
+
+[Repository 계층, 도메인과 영속성 엔티티 사이의 간극](https://kokodakadokok.tistory.com/entry/Repository-%EA%B3%84%EC%B8%B5-%EB%8F%84%EB%A9%94%EC%9D%B8%EA%B3%BC-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B0%84%EA%B7%B9-%EB%A7%A4%EA%BE%B8%EA%B8%B0)
+
+스프링은 기술적으로 편의를 위해서?
+데이터베이스 테이블과 Java의 class를 매핑해준 Jpa 기술을 사용하는 것으로 알고 있습니다.
+Jpa의 @Entity라는 annotation을 사용하여 정의하는 Class는 Domain과의 간극이 존재한다고
+생각합니다. 아직 이 간극에 대해 완벽히 알지 못하여서 Entity와 Domain을 혼돈해서 사용
+하게 되는거 같습니다. 최소한 Service를 나눌때 Domain기준으로 나누고 싶은데
+Entity(Table)단위로 나뉘게 되는 경향이 강한거 같습니다!
+지금은 TimeService와 ReservationService가 나뉘어져 있지만
+TimeService와 ReservationService가 하고 있는 역할은 결국 예약이라는 하나의 도메인에 속한다는 생각이 들어
+이 둘을 하나의 ReservationService로 합할까 고민하게 되었습니다.
+
+- Entity 패키지를 따로 둔 이유
+제가 따로 Entity 패키지를 분리한 이유는 Service, Repository, Contoller(controller에서 dto를 entity로 변환하기에) 모두다
+Entity를 바라보기 때문에 분리하게 되었습니다.
+
+
+## JDBC
+### 5단계
+- [x] 데이터베이스 설정
+- [x] 데이터베이스 연결
+
+### 6단계
+- [x] 데이터 조회하기
+
+### 7단계
+- [x] 데이터 추가하기
+- [x] 데이터 삭제하기
+- [x] 데이터 삭제 잘못된 요청시 예외처리
+
+### 7 단계 고민
+- Entity id에 setter 함수를 두면 위험성이 크다는 생각이 들어 새로운 객체를 생성해서 return 해야 되겠다 라는 생각을 가지게 되었습니다
+ 과연 spring은 새로 db에 생성된 entity에 관해 어떻게 id를 주입하나 궁금함군요 🤔
+
+### 질문사항
+- 어플리케이션 실행중 어디서든 exception이 발생하면 ControllerAdvice를 exception과 관련된 응답을 전달해줍니다.
+ 대부분의 exception을 최종적으로 ControllerAdvice에서 처리를 하다 보니 Service, Repository, Entity 등등 아무데서나 exception을
+ 던질 수 있겠다라는 착각을 하게 되는것 같습니다. 혹시 exception은 보통 어느 layer에서 처리를 하는지 궁금하며 예외처리는 어떻게 관리
+ 되는지 궁금합니다!
+
diff --git a/build.gradle b/build.gradle
index b11bfe1be..9912baed1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,11 +16,22 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-jdbc'
+ implementation 'org.springframework.boot:spring-boot-starter-log4j2'
+
+ runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
}
+configurations {
+ configureEach {
+ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
+ }
+}
+
test {
useJUnitPlatform()
}
diff --git a/src/main/java/roomescape/api/ReservationController.java b/src/main/java/roomescape/api/ReservationController.java
index 9ce39e3f0..3f2bef776 100644
--- a/src/main/java/roomescape/api/ReservationController.java
+++ b/src/main/java/roomescape/api/ReservationController.java
@@ -1,12 +1,13 @@
package roomescape.api;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import roomescape.api.dto.ReservationRequestDto;
import roomescape.api.dto.ReservationResponseDto;
import roomescape.entity.Reservation;
-import roomescape.repository.ReservationRepositoryImpl;
import roomescape.service.ReservationService;
import java.util.List;
@@ -16,30 +17,38 @@ public class ReservationController {
private final ReservationService reservationService;
- public ReservationController() {
- this.reservationService = new ReservationService(new ReservationRepositoryImpl());
+ public ReservationController(
+ @Autowired ReservationService reservationService
+ ) {
+ this.reservationService = reservationService;
}
@GetMapping("/reservations")
public ResponseEntity> readReservations() {
- List reservations = reservationService.readReservations();
+ return ResponseEntity
+ .ok()
+ .body(
+ reservationService.readReservations().stream().
+ map(ReservationResponseDto::fromEntity)
+ .toList()
+ );
+ }
- List response
- = reservations.stream()
- .map(ReservationResponseDto::fromEntity)
- .toList();
+ @GetMapping("/reservations/{id}")
+ public ResponseEntity readReservations(@PathVariable Long id) {
+ Reservation reservation = reservationService.readReservation(id);
return ResponseEntity
- .status(HttpStatus.OK)
- .body(response);
+ .ok()
+ .body(ReservationResponseDto.fromEntity(reservation));
}
@PostMapping("/reservations")
- public ResponseEntity createReservation(@RequestBody ReservationRequestDto reservationDto) {
- Reservation reservation = reservationService.createReservation(reservationDto.toEntity());
-
+ public ResponseEntity createReservation(
+ @RequestBody @Valid ReservationRequestDto reservationDto
+ ) {
ReservationResponseDto response =
- ReservationResponseDto.fromEntity(reservation);
+ ReservationResponseDto.fromEntity(reservationService.createReservation(reservationDto.toEntity()));
return ResponseEntity
.status(HttpStatus.CREATED)
@@ -50,8 +59,6 @@ public ResponseEntity createReservation(@RequestBody Res
@DeleteMapping("/reservations/{id}")
public ResponseEntity deleteReservation(@PathVariable Long id) {
reservationService.deleteReservation(id);
- return ResponseEntity
- .status(HttpStatus.NO_CONTENT)
- .build();
+ return ResponseEntity.noContent().build();
}
}
diff --git a/src/main/java/roomescape/api/StaticPageController.java b/src/main/java/roomescape/api/StaticPageController.java
index 369caa10d..d48fa7026 100644
--- a/src/main/java/roomescape/api/StaticPageController.java
+++ b/src/main/java/roomescape/api/StaticPageController.java
@@ -10,8 +10,14 @@ public String mainPage() {
return "home";
}
+
@GetMapping("/reservation")
public String reservationPage() {
- return "reservation";
+ return "new-reservation";
+ }
+
+ @GetMapping("/time")
+ public String timesPage() {
+ return "time";
}
}
diff --git a/src/main/java/roomescape/api/TimeController.java b/src/main/java/roomescape/api/TimeController.java
new file mode 100644
index 000000000..f95750372
--- /dev/null
+++ b/src/main/java/roomescape/api/TimeController.java
@@ -0,0 +1,57 @@
+package roomescape.api;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import roomescape.api.dto.TimeRequestDto;
+import roomescape.api.dto.TimeResponseDto;
+import roomescape.entity.Time;
+import roomescape.service.TimeService;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/times")
+public class TimeController {
+
+ private final TimeService timeService;
+
+ public TimeController(
+ @Autowired TimeService timeService
+ ) {
+ this.timeService = timeService;
+ }
+ @GetMapping
+ public ResponseEntity> readTimes() {
+ List times = timeService.readReservationTimes();
+ List timeResponseDtos = times.stream()
+ .map(TimeResponseDto::fromEntity)
+ .toList();
+
+ return ResponseEntity
+ .ok()
+ .body(timeResponseDtos);
+ }
+
+ @PostMapping
+ public ResponseEntity createTime(@RequestBody TimeRequestDto timeRequestDto) {
+ Time time = timeService.createReservationTime(TimeRequestDto.toEntity(timeRequestDto));
+ TimeResponseDto timeResponseDto = TimeResponseDto.fromEntity(time);
+
+ String headerName = "Location";
+ String headerValue = "/times/" + timeResponseDto.id();
+
+ return ResponseEntity
+ .status(HttpStatus.CREATED)
+ .header(headerName, headerValue)
+ .body(timeResponseDto);
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteTime(@PathVariable Long id) {
+ timeService.deleteReservationTime(id);
+
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/src/main/java/roomescape/api/dto/ReservationRequestDto.java b/src/main/java/roomescape/api/dto/ReservationRequestDto.java
index 908b2ad38..59384dd9c 100644
--- a/src/main/java/roomescape/api/dto/ReservationRequestDto.java
+++ b/src/main/java/roomescape/api/dto/ReservationRequestDto.java
@@ -1,43 +1,29 @@
package roomescape.api.dto;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
import roomescape.entity.Person;
import roomescape.entity.Reservation;
-import roomescape.util.DateTimeFormat;
+import roomescape.entity.Time;
+import roomescape.util.CustomDateTimeFormat;
import java.time.LocalDate;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
public record ReservationRequestDto(
+ @NotBlank
+ @NotNull
String name,
+ @NotBlank
+ @NotNull
String date,
- String time
+ @NotNull
+ Long time
) {
-
- public ReservationRequestDto {
- checkValidation(name, date, time);
- }
-
- private void checkValidation(String name, String date, String time) {
- blanksValidation(name, date, time);
- }
-
- private void blanksValidation(String... fields) {
- boolean isBlankExists = Arrays.stream(fields)
- .anyMatch(String::isBlank);
-
- if (isBlankExists) {
- throw new IllegalArgumentException("요청 인자의 값을 빈값일 수 없습니다");
- }
- }
-
-
public Reservation toEntity() {
return new Reservation(
new Person(this.name),
- LocalDate.parse(date, DateTimeFormatter.ofPattern(DateTimeFormat.dateFormat)),
- LocalTime.parse(time, DateTimeFormatter.ofPattern(DateTimeFormat.timeFormat))
+ LocalDate.parse(date, CustomDateTimeFormat.dateFormatter),
+ new Time(time)
);
}
}
diff --git a/src/main/java/roomescape/api/dto/ReservationResponseDto.java b/src/main/java/roomescape/api/dto/ReservationResponseDto.java
index 38b27d997..3fa901522 100644
--- a/src/main/java/roomescape/api/dto/ReservationResponseDto.java
+++ b/src/main/java/roomescape/api/dto/ReservationResponseDto.java
@@ -1,21 +1,19 @@
package roomescape.api.dto;
import roomescape.entity.Reservation;
-import roomescape.util.DateTimeFormat;
-
-import java.time.format.DateTimeFormatter;
+import roomescape.util.CustomDateTimeFormat;
public record ReservationResponseDto(
Long id,
String name,
String date,
- String time
+ TimeResponseDto time
) {
public static ReservationResponseDto fromEntity(Reservation reservation) {
return new ReservationResponseDto(
reservation.getId(),
reservation.getPerson().getName(),
- reservation.getDate().format(DateTimeFormatter.ofPattern(DateTimeFormat.dateFormat)),
- reservation.getTime().format(DateTimeFormatter.ofPattern(DateTimeFormat.timeFormat))
+ reservation.getDate().format(CustomDateTimeFormat.dateFormatter),
+ TimeResponseDto.fromEntity(reservation.getTime())
);
}
}
diff --git a/src/main/java/roomescape/api/dto/TimeRequestDto.java b/src/main/java/roomescape/api/dto/TimeRequestDto.java
new file mode 100644
index 000000000..f6007d0c8
--- /dev/null
+++ b/src/main/java/roomescape/api/dto/TimeRequestDto.java
@@ -0,0 +1,15 @@
+package roomescape.api.dto;
+
+import roomescape.entity.Time;
+
+import java.time.LocalTime;
+
+public record TimeRequestDto(
+ String time
+) {
+ public static Time toEntity(TimeRequestDto timeRequestDto) {
+ return new Time(
+ LocalTime.parse(timeRequestDto.time)
+ );
+ }
+}
diff --git a/src/main/java/roomescape/api/dto/TimeResponseDto.java b/src/main/java/roomescape/api/dto/TimeResponseDto.java
new file mode 100644
index 000000000..420babc85
--- /dev/null
+++ b/src/main/java/roomescape/api/dto/TimeResponseDto.java
@@ -0,0 +1,17 @@
+package roomescape.api.dto;
+
+import roomescape.entity.Time;
+import roomescape.util.CustomDateTimeFormat;
+
+public record TimeResponseDto(
+ Long id,
+ String time
+) {
+
+ public static TimeResponseDto fromEntity(Time reservationTime) {
+ return new TimeResponseDto(
+ reservationTime.getId(),
+ reservationTime.getTime().format(CustomDateTimeFormat.timeFormatter)
+ );
+ }
+}
diff --git a/src/main/java/roomescape/entity/Reservation.java b/src/main/java/roomescape/entity/Reservation.java
index ec4cf9f08..90028ec4e 100644
--- a/src/main/java/roomescape/entity/Reservation.java
+++ b/src/main/java/roomescape/entity/Reservation.java
@@ -2,33 +2,33 @@
import java.time.LocalDate;
import java.time.LocalTime;
+import java.util.Objects;
-public class Reservation implements Comparable {
+public class Reservation {
private Long id;
private Person person;
private LocalDate date;
- private LocalTime time;
+ private Time time;
- public Reservation(Long id, Person person, LocalDate date, LocalTime time) {
+ public Reservation(Long id, Person person, LocalDate date, Time time) {
this.id = id;
this.person = person;
this.date = date;
this.time = time;
}
- public Reservation(Person person, LocalDate date, LocalTime time) {
+
+ public Reservation(Person person, LocalDate date, Time time) {
this.id = 0L;
this.person = person;
this.date = date;
this.time = time;
}
- @Override
- public int compareTo(Reservation o) {
- return (int) (this.id - o.id);
- }
-
- public void setId(Long id) {
- this.id = id;
+ public void validateReservationDate() {
+ LocalDate today = LocalDate.now();
+ if (today.isAfter(date)) {
+ throw new IllegalArgumentException("시간이 지난 예약은 예약할 수 없습니다.");
+ }
}
public Long getId() {
@@ -39,11 +39,36 @@ public Person getPerson() {
return person;
}
+ public String getPersonName() {
+ return person.getName();
+ }
+
public LocalDate getDate() {
return date;
}
- public LocalTime getTime() {
+ public Time getTime() {
return time;
}
+
+ public LocalTime getLocalTime() {
+ return time.getTime();
+ }
+
+ public Long getTimeId() {
+ return time.getId();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Reservation that = (Reservation) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
}
diff --git a/src/main/java/roomescape/entity/Time.java b/src/main/java/roomescape/entity/Time.java
new file mode 100644
index 000000000..f8bebf148
--- /dev/null
+++ b/src/main/java/roomescape/entity/Time.java
@@ -0,0 +1,43 @@
+package roomescape.entity;
+
+import java.time.LocalTime;
+import java.util.Objects;
+
+public class Time {
+ private Long id;
+ private LocalTime time;
+
+ public Time(Long id, LocalTime time) {
+ this.id = id;
+ this.time = time;
+ }
+
+ public Time(LocalTime time) {
+ this.time = time;
+ }
+
+ public Time(Long id) {
+ this.id = id;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public LocalTime getTime() {
+ return time;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Time time = (Time) o;
+ return Objects.equals(id, time.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+}
diff --git a/src/main/java/roomescape/exception/EntityAlreadyExistException.java b/src/main/java/roomescape/exception/EntityAlreadyExistException.java
new file mode 100644
index 000000000..6de751b4b
--- /dev/null
+++ b/src/main/java/roomescape/exception/EntityAlreadyExistException.java
@@ -0,0 +1,7 @@
+package roomescape.exception;
+
+public class EntityAlreadyExistException extends BusinessException {
+ public EntityAlreadyExistException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/exception/GlobalExceptionHandler.java
index 555663f97..90a68e81e 100644
--- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java
+++ b/src/main/java/roomescape/exception/GlobalExceptionHandler.java
@@ -1,24 +1,71 @@
package roomescape.exception;
+import org.slf4j.Logger;
+import org.springframework.beans.TypeMismatchException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
+import roomescape.util.LoggerUtils;
@ControllerAdvice
public class GlobalExceptionHandler {
+ private final Logger logger = LoggerUtils.logger(GlobalExceptionHandler.class);
+
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
return ResponseEntity.badRequest().body(e.getMessage());
}
@ExceptionHandler(EntityNotFoundException.class)
- public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e) {
+ public ResponseEntity handleIllegalArgumentException(EntityNotFoundException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
+ return ResponseEntity.badRequest().body(e.getMessage());
+ }
+
+ @ExceptionHandler(EntityAlreadyExistException.class)
+ public ResponseEntity handleIllegalArgumentException(EntityAlreadyExistException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
+ return ResponseEntity.badRequest().body(e.getMessage());
+ }
+
+ @ExceptionHandler(TypeMismatchException.class)
+ public ResponseEntity handleIllegalArgumentException(TypeMismatchException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
+ return ResponseEntity.badRequest().body(e.getMessage());
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
+ return ResponseEntity.badRequest().body(e.getMessage());
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
return ResponseEntity.badRequest().body(e.getMessage());
}
-// @ExceptionHandler(Exception.class)
-// public ResponseEntity handleException(Exception e) {
-// return ResponseEntity.internalServerError().body(e.getMessage());
-// }
+ @ExceptionHandler(BusinessException.class)
+ public ResponseEntity handleBusinessException(BusinessException e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleException(Exception e) {
+ logger.error(e.getMessage());
+ logger.error(e.getClass().getName());
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
}
diff --git a/src/main/java/roomescape/exception/TimeDeleteException.java b/src/main/java/roomescape/exception/TimeDeleteException.java
new file mode 100644
index 000000000..523e13769
--- /dev/null
+++ b/src/main/java/roomescape/exception/TimeDeleteException.java
@@ -0,0 +1,7 @@
+package roomescape.exception;
+
+public class TimeDeleteException extends BusinessException {
+ public TimeDeleteException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/roomescape/repository/InMemoryReservationRepositoryImpl.java b/src/main/java/roomescape/repository/InMemoryReservationRepositoryImpl.java
new file mode 100644
index 000000000..9910ae853
--- /dev/null
+++ b/src/main/java/roomescape/repository/InMemoryReservationRepositoryImpl.java
@@ -0,0 +1,56 @@
+package roomescape.repository;
+
+import roomescape.entity.Reservation;
+import roomescape.exception.EntityNotFoundException;
+import roomescape.service.ReservationRepository;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class InMemoryReservationRepositoryImpl implements ReservationRepository {
+ private final Map reservations;
+ private final AtomicLong id = new AtomicLong(1);
+
+ public InMemoryReservationRepositoryImpl() {
+ reservations = new HashMap<>();
+ }
+
+ @Override
+ public Reservation save(Reservation reservation) {
+ Long newId = id.getAndIncrement();
+
+ Reservation reservationWithId = new Reservation(
+ newId,
+ reservation.getPerson(),
+ reservation.getDate(),
+ reservation.getTime()
+ );
+ reservations.put(reservation.getId(), reservationWithId);
+
+ return reservationWithId;
+ }
+
+ @Override
+ public List findAll() {
+ return reservations.values()
+ .stream()
+ .sorted(Comparator.comparing(Reservation::getId).reversed())
+ .toList();
+ }
+
+ @Override
+ public Reservation findById(Long id) {
+ return reservations.get(id);
+ }
+
+ @Override
+ public void deleteById(Long id) {
+ if (!reservations.containsKey(id)) {
+ throw new EntityNotFoundException("예약이 존재하지 않습니다");
+ }
+ reservations.remove(id);
+ }
+}
diff --git a/src/main/java/roomescape/repository/ReservationDao.java b/src/main/java/roomescape/repository/ReservationDao.java
new file mode 100644
index 000000000..289f27069
--- /dev/null
+++ b/src/main/java/roomescape/repository/ReservationDao.java
@@ -0,0 +1,115 @@
+package roomescape.repository;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.jdbc.support.KeyHolder;
+import org.springframework.stereotype.Repository;
+import roomescape.entity.Person;
+import roomescape.entity.Reservation;
+import roomescape.entity.Time;
+import roomescape.exception.EntityNotFoundException;
+import roomescape.util.CustomDateTimeFormat;
+
+import java.sql.PreparedStatement;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public class ReservationDao {
+
+ @Autowired
+ private final JdbcTemplate jdbcTemplate;
+ private final RowMapper reservationTimeRowMapper = (rs, rowNum) ->
+ new Reservation(
+ rs.getLong(1),
+ new Person(rs.getString(2)),
+ LocalDate.parse(rs.getString(3), CustomDateTimeFormat.dateFormatter),
+ new Time(rs.getLong(4), LocalTime.parse(rs.getString(5), CustomDateTimeFormat.timeFormatter))
+ );
+ private final RowMapper reservationRowMapper = (rs, rowNum) ->
+ new Reservation(
+ rs.getLong(1),
+ new Person(rs.getString(2)),
+ LocalDate.parse(rs.getString(3), CustomDateTimeFormat.dateFormatter),
+ new Time(rs.getLong(4))
+ );
+
+ public ReservationDao(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public Reservation save(Reservation reservation, Long timeId) {
+ String query = "INSERT INTO RESERVATION (name, date, time_id) VALUES (?, ?, ?)";
+ KeyHolder keyHolder = new GeneratedKeyHolder();
+
+ jdbcTemplate.update(connection -> {
+ PreparedStatement ps = connection.prepareStatement(
+ query,
+ new String[]{"id"});
+ ps.setString(1, reservation.getPersonName());
+ ps.setString(2, reservation.getDate().format(CustomDateTimeFormat.dateFormatter));
+ ps.setLong(3, timeId);
+ return ps;
+ }, keyHolder);
+
+ Long id = keyHolder.getKey().longValue();
+
+ return new Reservation(
+ id,
+ reservation.getPerson(),
+ reservation.getDate(),
+ reservation.getTime()
+ );
+ }
+
+ public List findAll() {
+ String query = "SELECT r.id as reservation_id, r.name, r.date, t.id as time_id, t.time as time_value FROM reservation as r inner join time as t on r.time_id = t.id";
+
+ return jdbcTemplate.query(query, reservationTimeRowMapper);
+ }
+
+ public List findByDate(LocalDate date) {
+ String query = "SELECT r.id as reservation_id, r.name, r.date, t.id as time_id, t.time as time_value FROM reservation as r inner join time as t on r.time_id = t.id where r.date = ?";
+ return jdbcTemplate.query(query, reservationTimeRowMapper, date.format(CustomDateTimeFormat.dateFormatter));
+ }
+
+ public Reservation findById(Long id) {
+ String query = "SELECT * FROM RESERVATION WHERE id = ?";
+
+ try {
+ return jdbcTemplate.queryForObject(query, reservationTimeRowMapper, id);
+ } catch (EmptyResultDataAccessException e) {
+ throw new EntityNotFoundException("해당 예약은 존재하지 않습니다.");
+ }
+ }
+
+ public void deleteById(Long id) {
+ String query = "delete from RESERVATION where id = ?";
+ int count = jdbcTemplate.update(query, id);
+
+ if (count == 0) {
+ throw new EntityNotFoundException("해당 id의 예약은 존재하지 않습니다");
+ }
+ }
+
+ public List findByTimeId(Long timeId) {
+ String query = "SELECT * FROM RESERVATION WHERE time_id = ?";
+ return jdbcTemplate.query(query, reservationRowMapper, timeId);
+ }
+
+ public Optional findByDateAndTime(LocalDate date, Long timeId) {
+ try {
+ String query = "SELECT * FROM RESERVATION WHERE date = ? AND time_id = ?";
+
+ return Optional.ofNullable(jdbcTemplate.queryForObject(query, reservationRowMapper, date, timeId));
+ } catch (EmptyResultDataAccessException e) {
+ return Optional.empty();
+ }
+
+ }
+}
diff --git a/src/main/java/roomescape/repository/ReservationRepositoryImpl.java b/src/main/java/roomescape/repository/ReservationRepositoryImpl.java
deleted file mode 100644
index c5dffe5ef..000000000
--- a/src/main/java/roomescape/repository/ReservationRepositoryImpl.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package roomescape.repository;
-
-import roomescape.entity.Person;
-import roomescape.entity.Reservation;
-import roomescape.exception.EntityNotFoundException;
-import roomescape.service.ReservationRepository;
-
-import java.time.LocalDate;
-import java.time.LocalTime;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicLong;
-
-public class ReservationRepositoryImpl implements ReservationRepository {
- private final Map reservations;
- private final AtomicLong id = new AtomicLong(1);
-
- public ReservationRepositoryImpl() {
- reservations = initialReservationsSetting();
- }
-
- private Map initialReservationsSetting() {
- Person person = new Person("brown");
- Reservation reservation1 = new Reservation(
- id.getAndIncrement(),
- person,
- LocalDate.of(2023,1,1),
- LocalTime.of(10, 0,0)
- );
- Reservation reservation2 = new Reservation(
- id.getAndIncrement(),
- person,
- LocalDate.of(2023,1,2),
- LocalTime.of(11, 0,0)
- );
- Reservation reservation3 = new Reservation(
- id.getAndIncrement(),
- person,
- LocalDate.of(2023,1,3),
- LocalTime.of(12, 0,0)
- );
-
-
- return new HashMap<>() {{
- put(reservation1.getId(), reservation1);
- put(reservation2.getId(), reservation2);
- put(reservation3.getId(), reservation3);
- }};
- }
-
- @Override
- public Reservation save(Reservation reservation) {
- Long newId = id.getAndIncrement();
-
- reservation.setId(newId);
- reservations.put(newId, reservation);
-
- return reservation;
- }
-
- @Override
- public List findAll() {
- return reservations.values()
- .stream()
- .sorted()
- .toList();
- }
-
- @Override
- public Reservation findById(Long id) {
- return reservations.get(id);
- }
-
- @Override
- public void deleteById(Long id) {
- if (!reservations.containsKey(id)) {
- throw new EntityNotFoundException("예약이 존재하지 않습니다");
- }
- reservations.remove(id);
- }
-}
diff --git a/src/main/java/roomescape/repository/TimeDao.java b/src/main/java/roomescape/repository/TimeDao.java
new file mode 100644
index 000000000..fd482b17e
--- /dev/null
+++ b/src/main/java/roomescape/repository/TimeDao.java
@@ -0,0 +1,81 @@
+package roomescape.repository;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.jdbc.support.KeyHolder;
+import org.springframework.stereotype.Repository;
+import roomescape.entity.Time;
+import roomescape.exception.EntityAlreadyExistException;
+import roomescape.exception.EntityNotFoundException;
+import roomescape.util.CustomDateTimeFormat;
+
+import java.sql.PreparedStatement;
+import java.time.LocalTime;
+import java.util.List;
+
+@Repository
+public class TimeDao {
+
+ @Autowired
+ private final JdbcTemplate jdbcTemplate;
+ private final RowMapper timeRowMapper = (rs, rowNum) -> new roomescape.entity.Time(
+ rs.getLong("id"),
+ LocalTime.parse(rs.getString("time"), CustomDateTimeFormat.timeFormatter)
+ );
+
+ public TimeDao(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public List findAll() {
+ String query = "SELECT * FROM time";
+
+ return jdbcTemplate.query(query, timeRowMapper);
+ }
+
+ public Time save(Time reservationTime) {
+ try {
+ String query = "INSERT INTO time (time) VALUES (?)";
+ KeyHolder keyHolder = new GeneratedKeyHolder();
+
+ jdbcTemplate.update(connection -> {
+ PreparedStatement ps = connection.prepareStatement(
+ query,
+ new String[]{"id"});
+ ps.setString(1, reservationTime.getTime().format(CustomDateTimeFormat.timeFormatter));
+ return ps;
+ }, keyHolder);
+
+ Long id = keyHolder.getKey().longValue();
+
+ return new Time(
+ id,
+ reservationTime.getTime()
+ );
+ } catch (DuplicateKeyException e) {
+ throw new EntityAlreadyExistException(e.getMessage());
+ }
+ }
+
+ public void delete(Long id) {
+ String query = "DELETE FROM time WHERE id = ?";
+ int count = jdbcTemplate.update(query, id);
+
+ if (count == 0) {
+ throw new EntityNotFoundException("해당 id의 시간은 존재하지 않습니다");
+ }
+ }
+
+ public Time findById(Long id) {
+ try {
+ String query = "SELECT * FROM time WHERE id = ?";
+ return jdbcTemplate.queryForObject(query, timeRowMapper, id);
+ } catch (EmptyResultDataAccessException e) {
+ throw new EntityNotFoundException("해당 시간은 존재하지 않습니다.");
+ }
+ }
+}
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index fda69ebac..90a40af36 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,25 +1,79 @@
package roomescape.service;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
import roomescape.entity.Reservation;
+import roomescape.entity.Time;
+import roomescape.repository.ReservationDao;
+import roomescape.repository.TimeDao;
+import java.time.LocalDate;
+import java.time.LocalTime;
import java.util.List;
+@Service
public class ReservationService {
- private final ReservationRepository reservationRepository;
+ private static final int RESERVATION_RUNNING_TIME = 60;
+ private static final int RESERVATION_CLEAN_TIME = 30;
+ private final ReservationDao reservationDao;
+ private final TimeDao timeDao;
- public ReservationService(ReservationRepository reservationRepository) {
- this.reservationRepository = reservationRepository;
+ public ReservationService(
+ @Autowired ReservationDao reservationRepository,
+ @Autowired TimeDao timeDao
+ ) {
+ this.reservationDao = reservationRepository;
+ this.timeDao = timeDao;
}
public List readReservations() {
- return reservationRepository.findAll();
+ return reservationDao.findAll();
}
- public Reservation createReservation(Reservation reservation) {
- return reservationRepository.save(reservation);
+ public Reservation readReservation(Long id) {
+ return reservationDao.findById(id);
+ }
+
+ public Reservation createReservation(Reservation reservationDto) {
+ reservationDto.validateReservationDate();
+
+ Time time = timeDao.findById(reservationDto.getTimeId());
+
+ Reservation currReservation = new Reservation(
+ reservationDto.getPerson(),
+ reservationDto.getDate(),
+ time
+ );
+
+ List savedReservations = reservationDao.findByDate(currReservation.getDate());
+
+ validateReservationTime(currReservation, savedReservations);
+
+ return reservationDao.save(currReservation, currReservation.getTimeId());
}
public void deleteReservation(Long id) {
- reservationRepository.deleteById(id);
+ reservationDao.deleteById(id);
+ }
+
+ private void validateReservationTime(Reservation currReservation, List savedReservations) {
+ LocalTime currReservationTime = currReservation.getLocalTime();
+
+ List savedTimes = savedReservations
+ .stream()
+ .map(Reservation::getLocalTime)
+ .toList();
+
+ long reservationLimitTime = RESERVATION_RUNNING_TIME + RESERVATION_CLEAN_TIME;
+
+ boolean conflictExists = savedTimes.stream().
+ anyMatch(s -> {
+ LocalTime upperBound = s.plusMinutes(reservationLimitTime);
+ return currReservationTime.equals(s) || (currReservationTime.isBefore(upperBound) && currReservationTime.isAfter(s));
+ });
+
+ if (conflictExists) {
+ throw new IllegalArgumentException("예약 불가능: 1시간 30분 이내의 예약이 이미 존재합니다.");
+ }
}
}
diff --git a/src/main/java/roomescape/service/TimeService.java b/src/main/java/roomescape/service/TimeService.java
new file mode 100644
index 000000000..3a5f260b2
--- /dev/null
+++ b/src/main/java/roomescape/service/TimeService.java
@@ -0,0 +1,42 @@
+package roomescape.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import roomescape.entity.Reservation;
+import roomescape.entity.Time;
+import roomescape.exception.TimeDeleteException;
+import roomescape.repository.ReservationDao;
+import roomescape.repository.TimeDao;
+
+import java.util.List;
+
+@Service
+public class TimeService {
+ private final ReservationDao reservationDao;
+ private final TimeDao timeDao;
+
+ public TimeService(
+ @Autowired ReservationDao reservationDao,
+ @Autowired TimeDao timeDao
+ ) {
+ this.reservationDao = reservationDao;
+ this.timeDao = timeDao;
+ }
+
+ public List readReservationTimes() {
+ return timeDao.findAll();
+ }
+
+ public Time createReservationTime(roomescape.entity.Time reservationTime) {
+ return timeDao.save(reservationTime);
+ }
+
+ public void deleteReservationTime(Long id) {
+ List reservations = reservationDao.findByTimeId(id);
+ if (!reservations.isEmpty()) {
+ throw new TimeDeleteException("해당 시간 id의 예약이 존재합니다");
+ }
+
+ timeDao.delete(id);
+ }
+}
diff --git a/src/main/java/roomescape/util/CustomDateTimeFormat.java b/src/main/java/roomescape/util/CustomDateTimeFormat.java
new file mode 100644
index 000000000..b121a0448
--- /dev/null
+++ b/src/main/java/roomescape/util/CustomDateTimeFormat.java
@@ -0,0 +1,8 @@
+package roomescape.util;
+
+import java.time.format.DateTimeFormatter;
+
+public class CustomDateTimeFormat {
+ public static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ public static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
+}
diff --git a/src/main/java/roomescape/util/LoggerUtils.java b/src/main/java/roomescape/util/LoggerUtils.java
new file mode 100644
index 000000000..1ff20b597
--- /dev/null
+++ b/src/main/java/roomescape/util/LoggerUtils.java
@@ -0,0 +1,11 @@
+package roomescape.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggerUtils {
+ public static Logger logger(Class clazz) {
+ return LoggerFactory.getLogger(clazz);
+ }
+}
+
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 000000000..e863df8be
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,12 @@
+spring:
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
+ datasource:
+ url: jdbc:h2:mem:database
+ jpa:
+ defer-datasource-initialization: true
+ sql:
+ init:
+ mode: always
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
new file mode 100644
index 000000000..873772572
--- /dev/null
+++ b/src/main/resources/data.sql
@@ -0,0 +1,6 @@
+insert into time (time) values ('10:00');
+insert into time (time) values ('10:30');
+insert into time (time) values ('11:00');
+insert into time (time) values ('12:00');
+insert into time (time) values ('13:00');
+insert into reservation (name, date, time_id) values ('sj', '2025-01-27', 1);
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..08179c971
--- /dev/null
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +1,17 @@
+
+
+
+ %d{HH:mm:ss.SSSZ} [%t] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 000000000..5f5fdc2e6
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,16 @@
+CREATE TABLE time
+(
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ time VARCHAR(255) NOT NULL UNIQUE,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE reservation
+(
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ name VARCHAR(255) NOT NULL,
+ date VARCHAR(255) NOT NULL,
+ time_id BIGINT,
+ PRIMARY KEY (id),
+ FOREIGN KEY (time_id) REFERENCES time (id)
+);
diff --git a/src/test/java/roomescape/CoreStepTest.java b/src/test/java/roomescape/CoreStepTest.java
new file mode 100644
index 000000000..886f2c917
--- /dev/null
+++ b/src/test/java/roomescape/CoreStepTest.java
@@ -0,0 +1,84 @@
+package roomescape;
+
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.annotation.DirtiesContext;
+import roomescape.api.ReservationController;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.core.Is.is;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+@SuppressWarnings("NonAsciiCharacters")
+public class CoreStepTest {
+
+ @Test
+ void 팔단계() {
+ Map params = new HashMap<>();
+ params.put("time", "14:00");
+
+ RestAssured.given().log().all()
+ .contentType(ContentType.JSON)
+ .body(params)
+ .when().post("/times")
+ .then().log().all()
+ .statusCode(201)
+ .header("Location", "/times/6");
+
+ RestAssured.given().log().all()
+ .when().get("/times")
+ .then().log().all()
+ .statusCode(200)
+ .body("size()", is(6));
+
+ RestAssured.given().log().all()
+ .when().delete("/times/3")
+ .then().log().all()
+ .statusCode(204);
+ }
+
+ @Test
+ void 구단계() {
+ Map reservation = new HashMap<>();
+ reservation.put("name", "브라운");
+ reservation.put("date", "2025-10-27");
+ reservation.put("time", "10:00");
+
+ RestAssured.given().log().all()
+ .contentType(ContentType.JSON)
+ .body(reservation)
+ .when().post("/reservations")
+ .then().log().all()
+ .statusCode(400);
+ }
+
+ @Nested
+ class 십단계_클라스 {
+ @Autowired
+ private ReservationController reservationController;
+
+ @Test
+ void 십단계() {
+ boolean isJdbcTemplateInjected = false;
+
+ for (Field field : reservationController.getClass().getDeclaredFields()) {
+ if (field.getType().equals(JdbcTemplate.class)) {
+ isJdbcTemplateInjected = true;
+ break;
+ }
+ }
+
+ assertThat(isJdbcTemplateInjected).isFalse();
+ }
+ }
+}
diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java
deleted file mode 100644
index b83918cb6..000000000
--- a/src/test/java/roomescape/MissionStepTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package roomescape;
-
-import io.restassured.RestAssured;
-import io.restassured.http.ContentType;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.annotation.DirtiesContext;
-
-import java.util.Map;
-
-import static org.hamcrest.core.Is.is;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
-@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
-@SuppressWarnings("NonAsciiCharacters")
-public class MissionStepTest {
-
- @Test
- void 일단계() {
- RestAssured.given().log().all()
- .when().get("/")
- .then().log().all()
- .statusCode(200);
- }
-
- @Nested
- class 이단계 {
- @Test
- void 예약_페이지_요청() {
- RestAssured.given().log().all()
- .when().get("/reservation")
- .then().log().all()
- .statusCode(200);
- }
-
- @Test
- void 전체_예약_가져오기_요청() {
- RestAssured.given().log().all()
- .when().get("/reservations")
- .then().log().all()
- .statusCode(200)
- .body("size()", is(3));
- }
- }
-
- @Nested
- class 삼단계 {
- Map params = Map.of(
- "name", "브라운",
- "date", "2023-08-05",
- "time", "15:40"
- );
-
- @Test
- void 예약_추가_요청() {
- RestAssured.given().log().all()
- .contentType(ContentType.JSON)
- .body(params)
- .when().post("/reservations")
- .then().log().all()
- .statusCode(201)
- .header("Location", "/reservations/4")
- .body("id", is(4));
-
- RestAssured.given().log().all()
- .when().get("/reservations")
- .then().log().all()
- .statusCode(200)
- .body("size()", is(4));
- }
-
- @Test
- void 예약_삭제_요청() {
- 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(2));
- }
- }
-
- @Nested
- class 사단계 {
-
- @Test
- void 예약_요청_인자_테스트() {
- Map requestBody = Map.of(
- "name", "브라운",
- "date", "",
- "time", ""
- );
-
-
- RestAssured.given().log().all()
- .contentType(ContentType.JSON)
- .body(requestBody)
- .when().post("/reservations")
- .then().log().all()
- .statusCode(400);
- }
-
- @Test
- void 없는_예약_삭제_테스트() {
- RestAssured.given().log().all()
- .when().delete("/reservations/10")
- .then().log().all()
- .statusCode(400);
- }
- }
-}