From 64335cd85b2ce2c807e75c3758064005cea5b031 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 14:30:07 +0900
Subject: [PATCH 01/75] =?UTF-8?q?feat:=20=EC=9D=B4=EC=A0=84=20=EB=AF=B8?=
 =?UTF-8?q?=EC=85=98=20=EC=BD=94=EB=93=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?=
 =?UTF-8?q?=EA=B8=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                                     | 147 +++++++++++++++++
 .../config/TimeFormatterConfig.java           |  17 ++
 .../controller/AdminController.java           |  24 +++
 .../controller/ReservationController.java     |  38 +++++
 .../controller/ReservationTimeController.java |  38 +++++
 .../java/roomescape/domain/Reservation.java   |  97 ++++++++++++
 .../roomescape/domain/ReservationTime.java    |  58 +++++++
 .../roomescape/dto/ReservationRequest.java    |   6 +
 .../roomescape/dto/ReservationResponse.java   |   6 +
 .../dto/ReservationTimeRequest.java           |   6 +
 .../dto/ReservationTimeResponse.java          |   6 +
 .../JdbcTemplateReservationRepository.java    |  82 ++++++++++
 ...JdbcTemplateReservationTimeRepository.java |  51 ++++++
 .../repository/ReservationRepository.java     |  13 ++
 .../repository/ReservationTimeRepository.java |  13 ++
 .../service/ReservationService.java           |  42 +++++
 .../service/ReservationTimeService.java       |  36 +++++
 src/main/resources/schema.sql                 |  16 ++
 .../controller/AdminControllerTest.java       |  34 ++++
 .../controller/ReservationControllerTest.java |  97 ++++++++++++
 .../ReservationTimeControllerTest.java        |  78 +++++++++
 .../roomescape/domain/ReservationTest.java    |  21 +++
 .../roomescape/infra/DBConnectionTest.java    |  36 +++++
 .../integration/AdminIntegrationTest.java     | 148 ++++++++++++++++++
 .../CollectionReservationRepository.java      |  70 +++++++++
 .../CollectionReservationTimeRepository.java  |  46 ++++++
 ...JdbcTemplateReservationRepositoryTest.java |  68 ++++++++
 ...TemplateReservationTimeRepositoryTest.java |  68 ++++++++
 src/test/resources/application.properties     |   2 +
 src/test/resources/schema.sql                 |  16 ++
 30 files changed, 1380 insertions(+)
 create mode 100644 README.md
 create mode 100644 src/main/java/roomescape/config/TimeFormatterConfig.java
 create mode 100644 src/main/java/roomescape/controller/AdminController.java
 create mode 100644 src/main/java/roomescape/controller/ReservationController.java
 create mode 100644 src/main/java/roomescape/controller/ReservationTimeController.java
 create mode 100644 src/main/java/roomescape/domain/Reservation.java
 create mode 100644 src/main/java/roomescape/domain/ReservationTime.java
 create mode 100644 src/main/java/roomescape/dto/ReservationRequest.java
 create mode 100644 src/main/java/roomescape/dto/ReservationResponse.java
 create mode 100644 src/main/java/roomescape/dto/ReservationTimeRequest.java
 create mode 100644 src/main/java/roomescape/dto/ReservationTimeResponse.java
 create mode 100644 src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
 create mode 100644 src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
 create mode 100644 src/main/java/roomescape/repository/ReservationRepository.java
 create mode 100644 src/main/java/roomescape/repository/ReservationTimeRepository.java
 create mode 100644 src/main/java/roomescape/service/ReservationService.java
 create mode 100644 src/main/java/roomescape/service/ReservationTimeService.java
 create mode 100644 src/main/resources/schema.sql
 create mode 100644 src/test/java/roomescape/controller/AdminControllerTest.java
 create mode 100644 src/test/java/roomescape/controller/ReservationControllerTest.java
 create mode 100644 src/test/java/roomescape/controller/ReservationTimeControllerTest.java
 create mode 100644 src/test/java/roomescape/domain/ReservationTest.java
 create mode 100644 src/test/java/roomescape/infra/DBConnectionTest.java
 create mode 100644 src/test/java/roomescape/integration/AdminIntegrationTest.java
 create mode 100644 src/test/java/roomescape/repository/CollectionReservationRepository.java
 create mode 100644 src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
 create mode 100644 src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
 create mode 100644 src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
 create mode 100644 src/test/resources/application.properties
 create mode 100644 src/test/resources/schema.sql

diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..08500f40f5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,147 @@
+# 요구사항 문서
+
+- [x] localhost:8080/admin 요청 시 어드민 메인 페이지가 응답할 수 있도록 구현한다.
+- [x] 어드민 메인 페이지는 templates/admin/index.html 파일을 이용한다.
+- [x] localhost:8080/admin/reservation 요청 시 아래 화면과 같이 예약 관리 페이지가 응답할 수 있도록 구현한다.
+- [x] 페이지는 templates/admin/reservation-legacy.html 파일을 이용한다.
+- [x] 예약 조회 API 명세를 따라 예약 관리 페이지 로드 시 호출되는 예약 목록 조회 API를 구현한다.
+- [x] API 명세를 따라 예약 추가 API 와 삭제 API를 구현한다.
+- [x] 예약 추가와 취소가 잘 동작한다.
+- [x] 이상의 요구 사항을 데이터베이스와 연동하도록 한다.
+
+- [x] 방탈출 시간표에 따라 방탈출 예약 시 시간을 선택하는 방식으로 수정한다.
+- [x] API 명세를 따라 시간 관리 API를 구현한다.
+- [x] 페이지는 templates/admin/time.html 파일을 이용한다.
+
+- [x] 기존에 구현한 예약 기능에서 시간을 시간 테이블에 저장된 값만 선택할 수 있도록 수정한다.
+- [x] templates/admin/reservation.html 을 사용한다.
+
+- [x] 레이어드 아키텍처를 적용하여 레이어별 책임과 역할에 따라 클래스를 분리한다.
+- [x] 분리한 클래스는 매번 새로 생성하지 않고 스프링 빈으로 등록한다.
+
+# API 명세
+
+## 예약 조회 API
+
+### Request
+
+> GET /reservations HTTP/1.1
+
+### Response
+
+> HTTP/1.1 200
+>
+> Content-Type: application/json
+
+``` JSON 
+[
+    {
+        "id": 1,
+        "name": "브라운",
+        "date": "2023-08-05",
+        "time": {
+            "id": 1,
+            "startAt": "10:00"
+        }
+    }
+]
+```
+
+## 예약 추가 API
+
+### Request
+
+> POST /reservations HTTP/1.1
+>
+> content-type: application/json
+
+```JSON
+{
+  "date": "2023-08-05",
+  "name": "브라운",
+  "timeId": 1
+}
+```
+
+### Response
+
+> HTTP/1.1 200
+>
+> Content-Type: application/json
+
+```JSON
+{
+  "id": 1,
+  "name": "브라운",
+  "date": "2023-08-05",
+  "time": {
+    "id": 1,
+    "startAt": "10:00"
+  }
+}
+```
+
+## 예약 취소 API
+
+### Request
+
+> DELETE /reservations/1 HTTP/1.1
+
+### Response
+
+> HTTP/1.1 200
+
+## 시간 추가 API
+
+### request
+
+> POST /times HTTP/1.1
+> content-type: application/json
+
+```JSON
+{
+  "startAt": "10:00"
+}
+```
+
+### response
+
+> HTTP/1.1 200
+> Content-Type: application/json
+
+```JSON
+{
+  "id": 1,
+  "startAt": "10:00"
+}
+```
+
+## 시간 조회 API
+
+### request
+
+> GET /times HTTP/1.1
+
+### response
+
+> HTTP/1.1 200
+> Content-Type: application/json
+
+```JSON
+[
+  {
+    "id": 1,
+    "startAt": "10:00"
+  }
+]
+```
+
+## 시간 삭제 API
+
+### request
+
+> DELETE /times/1 HTTP/1.1
+
+### response
+
+> HTTP/1.1 200
diff --git a/src/main/java/roomescape/config/TimeFormatterConfig.java b/src/main/java/roomescape/config/TimeFormatterConfig.java
new file mode 100644
index 0000000000..35c20b11b1
--- /dev/null
+++ b/src/main/java/roomescape/config/TimeFormatterConfig.java
@@ -0,0 +1,17 @@
+package roomescape.config;
+
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+import java.time.format.DateTimeFormatter;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class TimeFormatterConfig {
+    private static final String TIME_FORMAT = "HH:mm";
+
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer localTimeSerializerCustomizer() {
+        return builder -> builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
+    }
+}
diff --git a/src/main/java/roomescape/controller/AdminController.java b/src/main/java/roomescape/controller/AdminController.java
new file mode 100644
index 0000000000..cbdee6650a
--- /dev/null
+++ b/src/main/java/roomescape/controller/AdminController.java
@@ -0,0 +1,24 @@
+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 mainPage() {
+        return "admin/index";
+    }
+
+    @GetMapping("/reservation")
+    public String reservationPage() {
+        return "admin/reservation";
+    }
+
+    @GetMapping("/time")
+    public String reservationTimePage() {
+        return "admin/time";
+    }
+}
diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java
new file mode 100644
index 0000000000..26be3989e9
--- /dev/null
+++ b/src/main/java/roomescape/controller/ReservationController.java
@@ -0,0 +1,38 @@
+package roomescape.controller;
+
+import java.util.List;
+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 roomescape.dto.ReservationRequest;
+import roomescape.dto.ReservationResponse;
+import roomescape.service.ReservationService;
+
+@RestController
+@RequestMapping("/reservations")
+public class ReservationController {
+    private final ReservationService reservationService;
+
+    public ReservationController(ReservationService reservationService) {
+        this.reservationService = reservationService;
+    }
+
+    @PostMapping
+    public ReservationResponse saveReservation(@RequestBody ReservationRequest reservationRequest) {
+        return reservationService.save(reservationRequest);
+    }
+
+    @GetMapping
+    public List<ReservationResponse> findAllReservations() {
+        return reservationService.findAll();
+    }
+
+    @DeleteMapping("/{id}")
+    public void delete(@PathVariable long id) {
+        reservationService.delete(id);
+    }
+}
diff --git a/src/main/java/roomescape/controller/ReservationTimeController.java b/src/main/java/roomescape/controller/ReservationTimeController.java
new file mode 100644
index 0000000000..21d0e51b7b
--- /dev/null
+++ b/src/main/java/roomescape/controller/ReservationTimeController.java
@@ -0,0 +1,38 @@
+package roomescape.controller;
+
+import java.util.List;
+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 roomescape.dto.ReservationTimeRequest;
+import roomescape.dto.ReservationTimeResponse;
+import roomescape.service.ReservationTimeService;
+
+@RestController
+@RequestMapping("/times")
+public class ReservationTimeController {
+    private final ReservationTimeService reservationTimeService;
+
+    public ReservationTimeController(ReservationTimeService reservationTimeService) {
+        this.reservationTimeService = reservationTimeService;
+    }
+
+    @PostMapping
+    public ReservationTimeResponse save(@RequestBody ReservationTimeRequest reservationTimeRequest) {
+        return reservationTimeService.save(reservationTimeRequest);
+    }
+
+    @GetMapping
+    public List<ReservationTimeResponse> findAll() {
+        return reservationTimeService.findAll();
+    }
+
+    @DeleteMapping("/{id}")
+    public void delete(@PathVariable long id) {
+        reservationTimeService.delete(id);
+    }
+}
diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
new file mode 100644
index 0000000000..004dfd31ac
--- /dev/null
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -0,0 +1,97 @@
+package roomescape.domain;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Objects;
+
+public class Reservation implements Comparable<Reservation> {
+    private final Long id;
+    private final String name;
+    private final LocalDate date;
+    private final ReservationTime time;
+
+    public Reservation(long id, Reservation reservationBeforeSave) {
+        this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time);
+    }
+
+    public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
+        this.id = id;
+        this.name = name;
+        this.date = date;
+        this.time = time;
+    }
+
+    @Override
+    public int compareTo(Reservation other) {
+        LocalDateTime dateTime = LocalDateTime.of(date, time.getStartAt());
+        LocalDateTime otherDateTime = LocalDateTime.of(other.date, other.time.getStartAt());
+        return dateTime.compareTo(otherDateTime);
+    }
+
+    public boolean hasSameId(long id) {
+        return this.id == id;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public LocalDate getDate() {
+        return date;
+    }
+
+    public LocalTime getTime() {
+        return time.getStartAt();
+    }
+
+    public ReservationTime getReservationTime() {
+        return time;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + (date != null ? date.hashCode() : 0);
+        result = 31 * result + (time != null ? time.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Reservation that = (Reservation) o;
+
+        if (!Objects.equals(id, that.id)) {
+            return false;
+        }
+        if (!Objects.equals(name, that.name)) {
+            return false;
+        }
+        if (!Objects.equals(date, that.date)) {
+            return false;
+        }
+        return Objects.equals(time, that.time);
+    }
+
+    @Override
+    public String toString() {
+        return "Reservation{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                ", date=" + date +
+                ", time=" + time +
+                '}';
+    }
+}
diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
new file mode 100644
index 0000000000..8b54f99f81
--- /dev/null
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -0,0 +1,58 @@
+package roomescape.domain;
+
+import java.time.LocalTime;
+import java.util.Objects;
+
+public class ReservationTime {
+    private final Long id;
+    private final LocalTime startAt;
+
+    public ReservationTime(LocalTime startAt) {
+        this(null, startAt);
+    }
+
+    public ReservationTime(Long id, LocalTime startAt) {
+        this.id = id;
+        this.startAt = startAt;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public LocalTime getStartAt() {
+        return startAt;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (startAt != null ? startAt.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        ReservationTime that = (ReservationTime) o;
+
+        if (!Objects.equals(id, that.id)) {
+            return false;
+        }
+        return Objects.equals(startAt, that.startAt);
+    }
+
+    @Override
+    public String toString() {
+        return "ReservationTime{" +
+                "id=" + id +
+                ", startAt=" + startAt +
+                '}';
+    }
+}
diff --git a/src/main/java/roomescape/dto/ReservationRequest.java b/src/main/java/roomescape/dto/ReservationRequest.java
new file mode 100644
index 0000000000..ad5cb41a22
--- /dev/null
+++ b/src/main/java/roomescape/dto/ReservationRequest.java
@@ -0,0 +1,6 @@
+package roomescape.dto;
+
+import java.time.LocalDate;
+
+public record ReservationRequest(LocalDate date, String name, long timeId) {
+}
diff --git a/src/main/java/roomescape/dto/ReservationResponse.java b/src/main/java/roomescape/dto/ReservationResponse.java
new file mode 100644
index 0000000000..d9980c55fd
--- /dev/null
+++ b/src/main/java/roomescape/dto/ReservationResponse.java
@@ -0,0 +1,6 @@
+package roomescape.dto;
+
+import java.time.LocalDate;
+
+public record ReservationResponse(long id, String name, LocalDate date, ReservationTimeResponse time) {
+}
diff --git a/src/main/java/roomescape/dto/ReservationTimeRequest.java b/src/main/java/roomescape/dto/ReservationTimeRequest.java
new file mode 100644
index 0000000000..5cb43c58e1
--- /dev/null
+++ b/src/main/java/roomescape/dto/ReservationTimeRequest.java
@@ -0,0 +1,6 @@
+package roomescape.dto;
+
+import java.time.LocalTime;
+
+public record ReservationTimeRequest(LocalTime startAt) {
+}
diff --git a/src/main/java/roomescape/dto/ReservationTimeResponse.java b/src/main/java/roomescape/dto/ReservationTimeResponse.java
new file mode 100644
index 0000000000..2334e40c23
--- /dev/null
+++ b/src/main/java/roomescape/dto/ReservationTimeResponse.java
@@ -0,0 +1,6 @@
+package roomescape.dto;
+
+import java.time.LocalTime;
+
+public record ReservationTimeResponse(long id, LocalTime startAt) {
+}
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
new file mode 100644
index 0000000000..7884d19fdc
--- /dev/null
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -0,0 +1,82 @@
+package roomescape.repository;
+
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+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.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationRequest;
+
+@Repository
+public class JdbcTemplateReservationRepository implements ReservationRepository {
+    private final JdbcTemplate jdbcTemplate;
+
+    public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    @Override
+    public Reservation save(ReservationRequest reservationRequest) {
+        ReservationTime reservationTime = findReservationTime(reservationRequest);
+        Reservation reservation = new Reservation(null, reservationRequest.name(), reservationRequest.date(),
+                reservationTime);
+        KeyHolder keyHolder = new GeneratedKeyHolder();
+        save(reservation, keyHolder);
+        long id = keyHolder.getKey().longValue();
+        return new Reservation(id, reservation);
+    }
+
+    private ReservationTime findReservationTime(ReservationRequest reservationRequest) {
+        String reservationTimeSelectSql = "select * from reservation_time where id = ?";
+        return jdbcTemplate.queryForObject(reservationTimeSelectSql, (rs, rowNum) -> {
+            long id = rs.getLong(1);
+            LocalTime startAt = rs.getTime(2).toLocalTime();
+            return new ReservationTime(id, startAt);
+        }, reservationRequest.timeId());
+    }
+
+    private void save(Reservation reservation, KeyHolder keyHolder) {
+        jdbcTemplate.update(con -> {
+            String sql = "insert into reservation(name,date,time_id) values ( ?,?,? )";
+            PreparedStatement preparedStatement = con.prepareStatement(sql, new String[]{"id"});
+            preparedStatement.setString(1, reservation.getName());
+            preparedStatement.setDate(2, Date.valueOf(reservation.getDate()));
+            preparedStatement.setLong(3, reservation.getReservationTime().getId());
+            return preparedStatement;
+        }, keyHolder);
+    }
+
+    @Override
+    public List<Reservation> findAll() {
+        String query = "SELECT "
+                + "    r.id as reservation_id,"
+                + "    r.name,"
+                + "    r.date,"
+                + "    t.id as time_id,"
+                + "    t.start_at as time_value"
+                + " FROM reservation as r"
+                + " inner join reservation_time as t"
+                + " on r.time_id = t.id";
+        return jdbcTemplate.query(query,
+                (rs, rowNum) -> {
+                    long id = rs.getLong(1);
+                    String name = rs.getString(2);
+                    LocalDate date = rs.getDate(3).toLocalDate();
+                    long timeId = rs.getLong(4);
+                    LocalTime startAt = rs.getTime(5).toLocalTime();
+                    ReservationTime reservationTime = new ReservationTime(timeId, startAt);
+                    return new Reservation(id, name, date, reservationTime);
+                });
+    }
+
+    @Override
+    public void delete(long id) {
+        jdbcTemplate.update("delete from reservation where id = ?", id);
+    }
+}
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
new file mode 100644
index 0000000000..639db36eaa
--- /dev/null
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -0,0 +1,51 @@
+package roomescape.repository;
+
+import java.sql.PreparedStatement;
+import java.sql.Time;
+import java.time.LocalTime;
+import java.util.List;
+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.domain.ReservationTime;
+import roomescape.dto.ReservationTimeRequest;
+
+@Repository
+public class JdbcTemplateReservationTimeRepository implements ReservationTimeRepository {
+    private final JdbcTemplate jdbcTemplate;
+
+    public JdbcTemplateReservationTimeRepository(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    @Override
+    public ReservationTime save(ReservationTimeRequest reservationTimeRequest) {
+        KeyHolder keyHolder = new GeneratedKeyHolder();
+        save(reservationTimeRequest, keyHolder);
+        return new ReservationTime(keyHolder.getKey().longValue(), reservationTimeRequest.startAt());
+    }
+
+    private void save(ReservationTimeRequest reservationTimeRequest, KeyHolder keyHolder) {
+        jdbcTemplate.update(con -> {
+            PreparedStatement pstmt = con.prepareStatement("insert into reservation_time(start_at) values ( ? )",
+                    new String[]{"id"});
+            pstmt.setTime(1, Time.valueOf(reservationTimeRequest.startAt()));
+            return pstmt;
+        }, keyHolder);
+    }
+
+    @Override
+    public List<ReservationTime> findAll() {
+        return jdbcTemplate.query("select * from reservation_time", (rs, rowNum) -> {
+            long id = rs.getLong(1);
+            LocalTime time = rs.getTime(2).toLocalTime();
+            return new ReservationTime(id, time);
+        });
+    }
+
+    @Override
+    public void delete(long id) {
+        jdbcTemplate.update("delete from reservation_time where id = ?", id);
+    }
+}
diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java
new file mode 100644
index 0000000000..79c62ca32f
--- /dev/null
+++ b/src/main/java/roomescape/repository/ReservationRepository.java
@@ -0,0 +1,13 @@
+package roomescape.repository;
+
+import java.util.List;
+import roomescape.domain.Reservation;
+import roomescape.dto.ReservationRequest;
+
+public interface ReservationRepository {
+    Reservation save(ReservationRequest reservationRequest);
+
+    List<Reservation> findAll();
+
+    void delete(long id);
+}
diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java
new file mode 100644
index 0000000000..5f6e144eea
--- /dev/null
+++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java
@@ -0,0 +1,13 @@
+package roomescape.repository;
+
+import java.util.List;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationTimeRequest;
+
+public interface ReservationTimeRepository {
+    ReservationTime save(ReservationTimeRequest reservationTimeRequest);
+
+    List<ReservationTime> findAll();
+
+    void delete(long id);
+}
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
new file mode 100644
index 0000000000..de6d8d5288
--- /dev/null
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -0,0 +1,42 @@
+package roomescape.service;
+
+import java.util.List;
+import org.springframework.stereotype.Service;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationRequest;
+import roomescape.dto.ReservationResponse;
+import roomescape.dto.ReservationTimeResponse;
+import roomescape.repository.ReservationRepository;
+
+@Service
+public class ReservationService {
+    private final ReservationRepository reservationRepository;
+
+    public ReservationService(ReservationRepository reservationRepository) {
+        this.reservationRepository = reservationRepository;
+    }
+
+    public ReservationResponse save(ReservationRequest reservationRequest) {
+        Reservation saved = reservationRepository.save(reservationRequest);
+        return toResponse(saved);
+    }
+
+    private ReservationResponse toResponse(Reservation reservation) {
+        ReservationTime reservationTime = reservation.getReservationTime();
+        ReservationTimeResponse reservationTimeResponse = new ReservationTimeResponse(reservationTime.getId(),
+                reservation.getTime());
+        return new ReservationResponse(reservation.getId(),
+                reservation.getName(), reservation.getDate(), reservationTimeResponse);
+    }
+
+    public List<ReservationResponse> findAll() {
+        return reservationRepository.findAll().stream()
+                .map(this::toResponse)
+                .toList();
+    }
+
+    public void delete(long id) {
+        reservationRepository.delete(id);
+    }
+}
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
new file mode 100644
index 0000000000..908a747d0b
--- /dev/null
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -0,0 +1,36 @@
+package roomescape.service;
+
+import java.util.List;
+import org.springframework.stereotype.Service;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationTimeRequest;
+import roomescape.dto.ReservationTimeResponse;
+import roomescape.repository.ReservationTimeRepository;
+
+@Service
+public class ReservationTimeService {
+    private final ReservationTimeRepository reservationTimeRepository;
+
+    public ReservationTimeService(ReservationTimeRepository reservationTimeRepository) {
+        this.reservationTimeRepository = reservationTimeRepository;
+    }
+
+    public ReservationTimeResponse save(ReservationTimeRequest reservationTimeRequest) {
+        ReservationTime saved = reservationTimeRepository.save(reservationTimeRequest);
+        return toResponse(saved);
+    }
+
+    private ReservationTimeResponse toResponse(ReservationTime saved) {
+        return new ReservationTimeResponse(saved.getId(), saved.getStartAt());
+    }
+
+    public List<ReservationTimeResponse> findAll() {
+        return reservationTimeRepository.findAll().stream()
+                .map(this::toResponse)
+                .toList();
+    }
+
+    public void delete(long id) {
+        reservationTimeRepository.delete(id);
+    }
+}
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 0000000000..44a68814dd
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,16 @@
+CREATE TABLE IF NOT EXISTS reservation_time
+(
+    id   BIGINT       NOT NULL AUTO_INCREMENT,
+    start_at VARCHAR(255) NOT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS reservation
+(
+    id   BIGINT       NOT NULL AUTO_INCREMENT,
+    name VARCHAR(255) NOT NULL,
+    date VARCHAR(255) NOT NULL,
+    time_id BIGINT NOT NULL,                           -- 컬럼 수정
+    PRIMARY KEY (id),
+    FOREIGN KEY (time_id) REFERENCES reservation_time (id) -- 외래키 추가
+);
diff --git a/src/test/java/roomescape/controller/AdminControllerTest.java b/src/test/java/roomescape/controller/AdminControllerTest.java
new file mode 100644
index 0000000000..832a31fc0e
--- /dev/null
+++ b/src/test/java/roomescape/controller/AdminControllerTest.java
@@ -0,0 +1,34 @@
+package roomescape.controller;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class AdminControllerTest {
+    @Test
+    @DisplayName("관리자 메인 페이지 경로를 정해진 경로로 매핑한다.")
+    void mainPage() {
+        AdminController adminController = new AdminController();
+        String mainPage = adminController.mainPage();
+        Assertions.assertThat(mainPage)
+                .isEqualTo("admin/index");
+    }
+
+    @Test
+    @DisplayName("관리자 예약 정보 페이지 경로를 정해진 경로로 매핑한다.")
+    void reservationPage() {
+        AdminController adminController = new AdminController();
+        String reservationPage = adminController.reservationPage();
+        Assertions.assertThat(reservationPage)
+                .isEqualTo("admin/reservation");
+    }
+
+    @Test
+    @DisplayName("시간 관리 페이지 경로를 정해진 경로로 매핑한다.")
+    void reservationTimePage() {
+        AdminController adminController = new AdminController();
+        String reservationTimePage = adminController.reservationTimePage();
+        Assertions.assertThat(reservationTimePage)
+                .isEqualTo("admin/time");
+    }
+}
diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
new file mode 100644
index 0000000000..5f301b1993
--- /dev/null
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -0,0 +1,97 @@
+package roomescape.controller;
+
+import java.lang.reflect.Field;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationRequest;
+import roomescape.dto.ReservationResponse;
+import roomescape.dto.ReservationTimeResponse;
+import roomescape.repository.CollectionReservationRepository;
+import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.service.ReservationService;
+
+class ReservationControllerTest {
+    static final long timeId = 1L;
+    static final LocalTime time = LocalTime.now();
+    private final CollectionReservationTimeRepository timeRepository = new CollectionReservationTimeRepository(
+            new ArrayList<>(List.of(new ReservationTime(timeId, time)))
+    );
+
+    @Test
+    @DisplayName("예약 정보를 잘 저장하는지 확인한다.")
+    void saveReservation() {
+        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
+                timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationController reservationController = new ReservationController(reservationService);
+        LocalDate date = LocalDate.now();
+
+        ReservationResponse saveResponse = reservationController.saveReservation(
+                new ReservationRequest(date, "폴라", timeId));
+
+        long id = Objects.requireNonNull(saveResponse).id();
+        ReservationResponse expected = new ReservationResponse(id, "폴라", date,
+                new ReservationTimeResponse(timeId, time));
+
+        Assertions.assertThat(saveResponse)
+                .isEqualTo(expected);
+    }
+
+    @Test
+    @DisplayName("예약 정보를 잘 불러오는지 확인한다.")
+    void findAllReservations() {
+        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
+                timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationController reservationController = new ReservationController(reservationService);
+        List<ReservationResponse> allReservations = reservationController.findAllReservations();
+
+        Assertions.assertThat(allReservations)
+                .isEmpty();
+    }
+
+    @Test
+    @DisplayName("예약 정보를 잘 지우는지 확인한다.")
+    void delete() {
+        List<Reservation> reservations = List.of(
+                new Reservation(1L, "폴라", LocalDate.now(), new ReservationTime(LocalTime.now())));
+        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
+                new ArrayList<>(reservations), timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationController reservationController = new ReservationController(reservationService);
+
+        reservationController.delete(1L);
+        List<ReservationResponse> reservationResponses = reservationController.findAllReservations();
+
+        Assertions.assertThat(reservationResponses)
+                .isEmpty();
+    }
+
+    @Test
+    @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.")
+    void checkRepositoryDependency() {
+        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
+                timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationController reservationController = new ReservationController(reservationService);
+
+        boolean isRepositoryInjected = false;
+
+        for (Field field : reservationController.getClass().getDeclaredFields()) {
+            if (field.getType().getName().contains("Repository")) {
+                isRepositoryInjected = true;
+                break;
+            }
+        }
+
+        Assertions.assertThat(isRepositoryInjected).isFalse();
+    }
+}
diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
new file mode 100644
index 0000000000..84ad5b27ac
--- /dev/null
+++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
@@ -0,0 +1,78 @@
+package roomescape.controller;
+
+import java.lang.reflect.Field;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationTimeRequest;
+import roomescape.dto.ReservationTimeResponse;
+import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.service.ReservationTimeService;
+
+class ReservationTimeControllerTest {
+    @Test
+    @DisplayName("시간을 잘 저장하는지 확인한다.")
+    void save() {
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
+        LocalTime time = LocalTime.now();
+
+        ReservationTimeResponse save = reservationTimeController.save(new ReservationTimeRequest(time));
+
+        ReservationTimeResponse expected = new ReservationTimeResponse(save.id(), time);
+        Assertions.assertThat(save)
+                .isEqualTo(expected);
+    }
+
+    @Test
+    @DisplayName("시간을 잘 불러오는지 확인한다.")
+    void findAll() {
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
+        List<ReservationTimeResponse> reservationTimeResponses = reservationTimeController.findAll();
+
+        Assertions.assertThat(reservationTimeResponses)
+                .isEmpty();
+    }
+
+    @Test
+    @DisplayName("시간을 잘 지우는지 확인한다.")
+    void delete() {
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository(
+                new ArrayList<>(List.of(new ReservationTime(1L, LocalTime.now())))
+        );
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
+
+        reservationTimeController.delete(1);
+
+        List<ReservationTimeResponse> reservationTimeResponses = reservationTimeController.findAll();
+        Assertions.assertThat(reservationTimeResponses)
+                .isEmpty();
+    }
+
+    @Test
+    @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.")
+    void checkRepositoryDependency() {
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
+
+        boolean isRepositoryInjected = false;
+
+        for (Field field : reservationTimeController.getClass().getDeclaredFields()) {
+            if (field.getType().getName().contains("Repository")) {
+                isRepositoryInjected = true;
+                break;
+            }
+        }
+
+        Assertions.assertThat(isRepositoryInjected).isFalse();
+    }
+}
diff --git a/src/test/java/roomescape/domain/ReservationTest.java b/src/test/java/roomescape/domain/ReservationTest.java
new file mode 100644
index 0000000000..63bc689091
--- /dev/null
+++ b/src/test/java/roomescape/domain/ReservationTest.java
@@ -0,0 +1,21 @@
+package roomescape.domain;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class ReservationTest {
+    @Test
+    @DisplayName("날짜를 기준으로 비교를 잘 하는지 확인.")
+    void compareTo() {
+        Reservation first = new Reservation(1L, "폴라", LocalDate.of(1999, 12, 1), new ReservationTime(
+                LocalTime.of(16, 30)));
+        Reservation second = new Reservation(2L, "로빈", LocalDate.of(1998, 1, 8), new ReservationTime(
+                LocalTime.of(16, 30)));
+        int compareTo = first.compareTo(second);
+        Assertions.assertThat(compareTo)
+                .isGreaterThan(0);
+    }
+}
diff --git a/src/test/java/roomescape/infra/DBConnectionTest.java b/src/test/java/roomescape/infra/DBConnectionTest.java
new file mode 100644
index 0000000000..8f856916a1
--- /dev/null
+++ b/src/test/java/roomescape/infra/DBConnectionTest.java
@@ -0,0 +1,36 @@
+package roomescape.infra;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.junit.jupiter.api.DisplayName;
+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;
+
+@SpringBootTest
+public class DBConnectionTest {
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @Test
+    @DisplayName("데이터베이스 접속이 잘 되는지 확인.")
+    void connectToDB() {
+        try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
+            assertConnection(connection);
+        } catch (SQLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void assertConnection(Connection connection) {
+        assertAll(
+                () -> assertThat(connection).isNotNull(),
+                () -> assertThat(connection.getCatalog()).isEqualTo("TEST"),
+                () -> assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue()
+        );
+    }
+}
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
new file mode 100644
index 0000000000..068b0dbe25
--- /dev/null
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -0,0 +1,148 @@
+package roomescape.integration;
+
+import static org.hamcrest.Matchers.is;
+
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import java.util.HashMap;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+public class AdminIntegrationTest {
+    @LocalServerPort
+    private int port;
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @BeforeEach
+    void init() {
+        jdbcTemplate.update("delete from reservation");
+        jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
+        jdbcTemplate.update("delete from reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
+        jdbcTemplate.update("insert into reservation_time(start_at) values('11:56')");
+        RestAssured.port = port;
+    }
+
+    @Test
+    @DisplayName("관리자 메인 페이지가 잘 접속된다.")
+    void adminMainPageLoad() {
+        RestAssured.given().log().all()
+                .when().get("/admin")
+                .then().log().all()
+                .statusCode(200);
+    }
+
+    @Test
+    @DisplayName("관리자 예약 페이지가 잘 접속된다.")
+    void adminReservationPageLoad() {
+        RestAssured.given().log().all()
+                .when().get("/admin/reservation")
+                .then().log().all()
+                .statusCode(200);
+
+        RestAssured.given().log().all()
+                .when().get("/reservations")
+                .then().log().all()
+                .statusCode(200)
+                .body("size()", is(0));
+    }
+
+    @Test
+    @DisplayName("관리자 예약 페이지가 잘 동작한다.")
+    void adminReservationPageWork() {
+        Integer integer = jdbcTemplate.queryForObject("select id from reservation_time",
+                Integer.class);
+        System.out.println(integer);
+        Map<String, Object> params = new HashMap<>();
+        params.put("name", "브라운");
+        params.put("date", "2023-08-05");
+        params.put("timeId", 1);
+
+        RestAssured.given().log().all()
+                .contentType(ContentType.JSON)
+                .body(params)
+                .when().post("/reservations")
+                .then().log().all()
+                .statusCode(200)
+                .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(200);
+
+        RestAssured.given().log().all()
+                .when().get("/reservations")
+                .then().log().all()
+                .statusCode(200)
+                .body("size()", is(0));
+    }
+
+    @Test
+    @DisplayName("관리자 예약 페이지가 DB와 함께 잘 동작한다.")
+    void adminReservationPageWorkWithDB() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("name", "브라운");
+        params.put("date", "2023-08-05");
+        params.put("timeId", 1);
+
+        RestAssured.given().log().all()
+                .contentType(ContentType.JSON)
+                .body(params)
+                .when().post("/reservations")
+                .then().log().all()
+                .statusCode(200);
+
+        Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
+        Assertions.assertThat(count).isEqualTo(1);
+
+        RestAssured.given().log().all()
+                .when().delete("/reservations/1")
+                .then().log().all()
+                .statusCode(200);
+
+        Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
+        Assertions.assertThat(countAfterDelete).isEqualTo(0);
+    }
+
+    @Test
+    @DisplayName("시간 관리 페이지가 잘 동작한다.")
+    void reservationTimePageWork() {
+        Map<String, String> params = new HashMap<>();
+        params.put("startAt", "10:00");
+
+        RestAssured.given().log().all()
+                .contentType(ContentType.JSON)
+                .body(params)
+                .when().post("/times")
+                .then().log().all()
+                .statusCode(200);
+
+        RestAssured.given().log().all()
+                .when().get("/times")
+                .then().log().all()
+                .statusCode(200)
+                .body("size()", is(2));
+
+        RestAssured.given().log().all()
+                .when().delete("/times/2")
+                .then().log().all()
+                .statusCode(200);
+    }
+}
diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java
new file mode 100644
index 0000000000..087cc9de04
--- /dev/null
+++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java
@@ -0,0 +1,70 @@
+package roomescape.repository;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationRequest;
+
+public class CollectionReservationRepository implements ReservationRepository {
+    private final List<Reservation> reservations;
+    private final AtomicLong atomicLong;
+    private final CollectionReservationTimeRepository timeRepository;
+
+    public CollectionReservationRepository(CollectionReservationTimeRepository timeRepository) {
+        this(new ArrayList<>(), new AtomicLong(0), timeRepository);
+    }
+
+    public CollectionReservationRepository(List<Reservation> reservations, AtomicLong atomicLong,
+                                           CollectionReservationTimeRepository timeRepository) {
+        this.reservations = reservations;
+        this.atomicLong = atomicLong;
+        this.timeRepository = timeRepository;
+    }
+
+    public CollectionReservationRepository(List<Reservation> reservations,
+                                           CollectionReservationTimeRepository timeRepository) {
+        this(reservations, new AtomicLong(0), timeRepository);
+    }
+
+    @Override
+    public Reservation save(ReservationRequest reservationRequest) {
+        Reservation reservation = fromRequest(reservationRequest);
+        reservations.add(reservation);
+        return reservation;
+    }
+
+    private Reservation fromRequest(ReservationRequest reservationRequest) {
+        long id = atomicLong.incrementAndGet();
+
+        String name = reservationRequest.name();
+        LocalDate date = reservationRequest.date();
+        ReservationTime reservationTime = timeRepository.findAll().stream()
+                .filter(sameId(reservationRequest))
+                .findAny()
+                .orElseThrow();
+        return new Reservation(id, name, date, reservationTime);
+    }
+
+    private static Predicate<ReservationTime> sameId(ReservationRequest reservationRequest) {
+        return reservationTime -> reservationTime.getId() == reservationRequest.timeId();
+    }
+
+    @Override
+    public List<Reservation> findAll() {
+        return reservations.stream()
+                .sorted()
+                .toList();
+    }
+
+    @Override
+    public void delete(long id) {
+        reservations.stream()
+                .filter(reservation -> reservation.hasSameId(id))
+                .findAny()
+                .ifPresent(reservations::remove);
+    }
+}
diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
new file mode 100644
index 0000000000..b6f22fe988
--- /dev/null
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -0,0 +1,46 @@
+package roomescape.repository;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationTimeRequest;
+
+public class CollectionReservationTimeRepository implements ReservationTimeRepository {
+    private final List<ReservationTime> reservationTimes;
+    private final AtomicLong atomicLong;
+
+    public CollectionReservationTimeRepository() {
+        this(new ArrayList<>());
+    }
+
+    public CollectionReservationTimeRepository(List<ReservationTime> reservationTimes) {
+        this(reservationTimes, new AtomicLong(0));
+    }
+
+    public CollectionReservationTimeRepository(List<ReservationTime> reservationTimes, AtomicLong atomicLong) {
+        this.reservationTimes = reservationTimes;
+        this.atomicLong = atomicLong;
+    }
+
+    @Override
+    public ReservationTime save(ReservationTimeRequest reservationTimeRequest) {
+        ReservationTime reservationTime = new ReservationTime(atomicLong.incrementAndGet(),
+                reservationTimeRequest.startAt());
+        reservationTimes.add(reservationTime);
+        return reservationTime;
+    }
+
+    @Override
+    public List<ReservationTime> findAll() {
+        return List.copyOf(reservationTimes);
+    }
+
+    @Override
+    public void delete(long id) {
+        reservationTimes.stream()
+                .filter(reservationTime -> reservationTime.getId().equals(id))
+                .findAny()
+                .ifPresent(reservationTimes::remove);
+    }
+}
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
new file mode 100644
index 0000000000..c2acc8994f
--- /dev/null
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
@@ -0,0 +1,68 @@
+package roomescape.repository;
+
+import java.time.LocalDate;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+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 roomescape.domain.Reservation;
+import roomescape.dto.ReservationRequest;
+
+@SpringBootTest
+class JdbcTemplateReservationRepositoryTest {
+    @Autowired
+    private ReservationRepository reservationRepository;
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @BeforeEach
+    void init() {
+        jdbcTemplate.update("delete from reservation");
+        jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
+        jdbcTemplate.update("delete from reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
+        jdbcTemplate.update("insert into reservation_time(start_at) values('11:56')");
+    }
+
+    @Test
+    @DisplayName("Reservation 을 잘 저장하는지 확인한다.")
+    void save() {
+        var beforeSave = reservationRepository.findAll();
+        Reservation saved = reservationRepository.save(new ReservationRequest(LocalDate.now(), "test", 1));
+        var afterSave = reservationRepository.findAll();
+
+        Assertions.assertThat(afterSave)
+                .containsAll(beforeSave)
+                .contains(saved);
+    }
+
+    @Test
+    @DisplayName("Reservation 을 잘 조회하는지 확인한다.")
+    void findAll() {
+        List<Reservation> beforeSave = reservationRepository.findAll();
+        reservationRepository.save(new ReservationRequest(LocalDate.now(), "test", 1));
+        reservationRepository.save(new ReservationRequest(LocalDate.now(), "test2", 1));
+
+        List<Reservation> afterSave = reservationRepository.findAll();
+        Assertions.assertThat(afterSave.size())
+                .isEqualTo(beforeSave.size() + 2);
+    }
+
+    @Test
+    @DisplayName("Reservation 을 잘 지우는지 확인한다.")
+    void delete() {
+        List<Reservation> beforeSaveAndDelete = reservationRepository.findAll();
+        reservationRepository.save(new ReservationRequest(LocalDate.now(), "test", 1));
+
+        reservationRepository.delete(1L);
+
+        List<Reservation> afterSaveAndDelete = reservationRepository.findAll();
+
+        Assertions.assertThat(beforeSaveAndDelete)
+                .containsExactlyElementsOf(afterSaveAndDelete);
+    }
+}
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
new file mode 100644
index 0000000000..5bc5cd67f2
--- /dev/null
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
@@ -0,0 +1,68 @@
+package roomescape.repository;
+
+import java.time.LocalTime;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+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 roomescape.domain.ReservationTime;
+import roomescape.dto.ReservationTimeRequest;
+
+@SpringBootTest
+class JdbcTemplateReservationTimeRepositoryTest {
+    @Autowired
+    private ReservationTimeRepository reservationTimeRepository;
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @BeforeEach
+    void init() {
+        jdbcTemplate.update("delete from reservation");
+        jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
+        jdbcTemplate.update("delete from reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
+    }
+
+    @Test
+    @DisplayName("ReservationTime 을 잘 저장하는지 확인한다.")
+    void save() {
+        var beforeSave = reservationTimeRepository.findAll().stream().map(ReservationTime::getId).toList();
+        ReservationTime saved = reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+        var afterSave = reservationTimeRepository.findAll().stream().map(ReservationTime::getId).toList();
+
+        Assertions.assertThat(afterSave)
+                .containsAll(beforeSave)
+                .contains(saved.getId());
+    }
+
+    @Test
+    @DisplayName("ReservationTime 을 잘 조회하는지 확인한다.")
+    void findAll() {
+        List<ReservationTime> beforeSave = reservationTimeRepository.findAll();
+        reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+        reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+
+        List<ReservationTime> afterSave = reservationTimeRepository.findAll();
+
+        Assertions.assertThat(afterSave.size())
+                .isEqualTo(beforeSave.size() + 2);
+    }
+
+    @Test
+    @DisplayName("ReservationTime 을 잘 지우하는지 확인한다.")
+    void delete() {
+        List<ReservationTime> beforeSaveAndDelete = reservationTimeRepository.findAll();
+        reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+
+        reservationTimeRepository.delete(1L);
+
+        List<ReservationTime> afterSaveAndDelete = reservationTimeRepository.findAll();
+
+        Assertions.assertThat(beforeSaveAndDelete)
+                .containsExactlyInAnyOrderElementsOf(afterSaveAndDelete);
+    }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000000..b53e73970d
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,2 @@
+spring.h2.console.enabled=true
+spring.datasource.url=jdbc:h2:mem:test
diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql
new file mode 100644
index 0000000000..0945a159b6
--- /dev/null
+++ b/src/test/resources/schema.sql
@@ -0,0 +1,16 @@
+CREATE TABLE IF NOT EXISTS reservation_time
+(
+    id   BIGINT       NOT NULL AUTO_INCREMENT,
+    start_at VARCHAR(255) NOT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS 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 reservation_time (id) -- 외래키 추가
+);

From 9d575feb34bea987b14acdca5584211da84869e6 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 14:53:41 +0900
Subject: [PATCH 02/75] =?UTF-8?q?docs:=20=EC=9A=94=EA=B5=AC=EC=82=AC?=
 =?UTF-8?q?=ED=95=AD=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?=
 =?UTF-8?q?README.md=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 30 ++++++++++++------------------
 1 file changed, 12 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 08500f40f5..2cd133e7aa 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,17 @@
 # 요구사항 문서
 
-- [x] localhost:8080/admin 요청 시 어드민 메인 페이지가 응답할 수 있도록 구현한다.
-- [x] 어드민 메인 페이지는 templates/admin/index.html 파일을 이용한다.
-- [x] localhost:8080/admin/reservation 요청 시 아래 화면과 같이 예약 관리 페이지가 응답할 수 있도록 구현한다.
-- [x] 페이지는 templates/admin/reservation-legacy.html 파일을 이용한다.
-- [x] 예약 조회 API 명세를 따라 예약 관리 페이지 로드 시 호출되는 예약 목록 조회 API를 구현한다.
-- [x] API 명세를 따라 예약 추가 API 와 삭제 API를 구현한다.
-- [x] 예약 추가와 취소가 잘 동작한다.
-- [x] 이상의 요구 사항을 데이터베이스와 연동하도록 한다.
-
-- [x] 방탈출 시간표에 따라 방탈출 예약 시 시간을 선택하는 방식으로 수정한다.
-- [x] API 명세를 따라 시간 관리 API를 구현한다.
-- [x] 페이지는 templates/admin/time.html 파일을 이용한다.
-
-- [x] 기존에 구현한 예약 기능에서 시간을 시간 테이블에 저장된 값만 선택할 수 있도록 수정한다.
-- [x] templates/admin/reservation.html 을 사용한다.
-
-- [x] 레이어드 아키텍처를 적용하여 레이어별 책임과 역할에 따라 클래스를 분리한다.
-- [x] 분리한 클래스는 매번 새로 생성하지 않고 스프링 빈으로 등록한다.
+- [ ] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정
+- [ ] 예약 시간에 대한 제약 조건 추가
+    - [ ] 중복된 예약 시간 생성 요청 시 에러
+    - [ ] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
+    - [ ] 예약이 있는 예약 시간을 삭제 요청 시 에러
+    - [ ] 존재하지 않는 시간을 삭제 요청 시 에러
+- [ ] 예약에 대한 제약 조건 추가
+    - [ ] 동일한 날짜와 시간에 예약 생성 요청 시 에러
+    - [ ] 존재하지 않는 시간에 예약 생성 요청 시 에러
+    - [ ] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
+    - [ ] 존재하지 않는 예약을 삭제 요청 시 에러
+    - [ ] 지나간 날짜와 시간의 예약 요청 시 에러
 
 # API 명세
 

From 258c9b6dedb7c64604f62dbfcaf5e3cd82a9086c Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 15:12:00 +0900
Subject: [PATCH 03/75] =?UTF-8?q?fix:=20=EC=8B=9C=EA=B0=84=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20API=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 상태코드를 200 -> 201 변경
- Location 헤더 추가

Co-authored-by: zangsu <zangsu_@naver.com>
---
 .../roomescape/controller/ReservationTimeController.java  | 8 ++++++--
 .../controller/ReservationTimeControllerTest.java         | 2 +-
 .../java/roomescape/integration/AdminIntegrationTest.java | 2 +-
 3 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/main/java/roomescape/controller/ReservationTimeController.java b/src/main/java/roomescape/controller/ReservationTimeController.java
index 21d0e51b7b..0dd8f6c839 100644
--- a/src/main/java/roomescape/controller/ReservationTimeController.java
+++ b/src/main/java/roomescape/controller/ReservationTimeController.java
@@ -1,6 +1,8 @@
 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;
@@ -22,8 +24,10 @@ public ReservationTimeController(ReservationTimeService reservationTimeService)
     }
 
     @PostMapping
-    public ReservationTimeResponse save(@RequestBody ReservationTimeRequest reservationTimeRequest) {
-        return reservationTimeService.save(reservationTimeRequest);
+    public ResponseEntity<ReservationTimeResponse> save(@RequestBody ReservationTimeRequest reservationTimeRequest) {
+        ReservationTimeResponse saved = reservationTimeService.save(reservationTimeRequest);
+        return ResponseEntity.created(URI.create("/times/" + saved.id()))
+                .body(saved);
     }
 
     @GetMapping
diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
index 84ad5b27ac..d9b50035ca 100644
--- a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
@@ -22,7 +22,7 @@ void save() {
         ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
         LocalTime time = LocalTime.now();
 
-        ReservationTimeResponse save = reservationTimeController.save(new ReservationTimeRequest(time));
+        ReservationTimeResponse save = reservationTimeController.save(new ReservationTimeRequest(time)).getBody();
 
         ReservationTimeResponse expected = new ReservationTimeResponse(save.id(), time);
         Assertions.assertThat(save)
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
index 068b0dbe25..e27469203d 100644
--- a/src/test/java/roomescape/integration/AdminIntegrationTest.java
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -132,7 +132,7 @@ void reservationTimePageWork() {
                 .body(params)
                 .when().post("/times")
                 .then().log().all()
-                .statusCode(200);
+                .statusCode(201);
 
         RestAssured.given().log().all()
                 .when().get("/times")

From dbb1b8b482a9f3f19e9c97516c58753e610747fe Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 15:18:55 +0900
Subject: [PATCH 04/75] =?UTF-8?q?fix:=20=EC=98=88=EC=95=BD=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20API=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 상태코드를 200 -> 201 변경
- Location 헤더 추가

Co-authored-by: zangsu <zangsu_@naver.com>
---
 .../java/roomescape/controller/ReservationController.java | 8 ++++++--
 .../roomescape/controller/ReservationControllerTest.java  | 3 ++-
 .../java/roomescape/integration/AdminIntegrationTest.java | 4 ++--
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java
index 26be3989e9..a4db562d27 100644
--- a/src/main/java/roomescape/controller/ReservationController.java
+++ b/src/main/java/roomescape/controller/ReservationController.java
@@ -1,6 +1,8 @@
 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;
@@ -22,8 +24,10 @@ public ReservationController(ReservationService reservationService) {
     }
 
     @PostMapping
-    public ReservationResponse saveReservation(@RequestBody ReservationRequest reservationRequest) {
-        return reservationService.save(reservationRequest);
+    public ResponseEntity<ReservationResponse> saveReservation(@RequestBody ReservationRequest reservationRequest) {
+        ReservationResponse saved = reservationService.save(reservationRequest);
+        return ResponseEntity.created(URI.create("/reservations/" + saved.id()))
+                .body(saved);
     }
 
     @GetMapping
diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index 5f301b1993..37ddb85fbb 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -35,7 +35,8 @@ void saveReservation() {
         LocalDate date = LocalDate.now();
 
         ReservationResponse saveResponse = reservationController.saveReservation(
-                new ReservationRequest(date, "폴라", timeId));
+                        new ReservationRequest(date, "폴라", timeId))
+                .getBody();
 
         long id = Objects.requireNonNull(saveResponse).id();
         ReservationResponse expected = new ReservationResponse(id, "폴라", date,
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
index e27469203d..f94062788f 100644
--- a/src/test/java/roomescape/integration/AdminIntegrationTest.java
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -73,7 +73,7 @@ void adminReservationPageWork() {
                 .body(params)
                 .when().post("/reservations")
                 .then().log().all()
-                .statusCode(200)
+                .statusCode(201)
                 .body("id", is(1));
 
         RestAssured.given().log().all()
@@ -107,7 +107,7 @@ void adminReservationPageWorkWithDB() {
                 .body(params)
                 .when().post("/reservations")
                 .then().log().all()
-                .statusCode(200);
+                .statusCode(201);
 
         Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
         Assertions.assertThat(count).isEqualTo(1);

From 202216df6df9f5f87135d17024848961d3642ebe Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 15:22:28 +0900
Subject: [PATCH 05/75] =?UTF-8?q?fix:=20=EC=98=88=EC=95=BD,=20=EC=98=88?=
 =?UTF-8?q?=EC=95=BD=20=EC=8B=9C=EA=B0=84=20=EC=82=AD=EC=A0=9C=20API=20?=
 =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 상태코드를 200 -> 204 변경

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                                   | 2 +-
 .../java/roomescape/controller/ReservationController.java   | 3 ++-
 .../roomescape/controller/ReservationTimeController.java    | 3 ++-
 .../java/roomescape/integration/AdminIntegrationTest.java   | 6 +++---
 4 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 2cd133e7aa..1c39527256 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # 요구사항 문서
 
-- [ ] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정
+- [x] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정
 - [ ] 예약 시간에 대한 제약 조건 추가
     - [ ] 중복된 예약 시간 생성 요청 시 에러
     - [ ] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java
index a4db562d27..349124b6d0 100644
--- a/src/main/java/roomescape/controller/ReservationController.java
+++ b/src/main/java/roomescape/controller/ReservationController.java
@@ -36,7 +36,8 @@ public List<ReservationResponse> findAllReservations() {
     }
 
     @DeleteMapping("/{id}")
-    public void delete(@PathVariable long id) {
+    public ResponseEntity<Void> delete(@PathVariable long id) {
         reservationService.delete(id);
+        return ResponseEntity.noContent().build();
     }
 }
diff --git a/src/main/java/roomescape/controller/ReservationTimeController.java b/src/main/java/roomescape/controller/ReservationTimeController.java
index 0dd8f6c839..868e43f4b1 100644
--- a/src/main/java/roomescape/controller/ReservationTimeController.java
+++ b/src/main/java/roomescape/controller/ReservationTimeController.java
@@ -36,7 +36,8 @@ public List<ReservationTimeResponse> findAll() {
     }
 
     @DeleteMapping("/{id}")
-    public void delete(@PathVariable long id) {
+    public ResponseEntity<Void> delete(@PathVariable long id) {
         reservationTimeService.delete(id);
+        return ResponseEntity.noContent().build();
     }
 }
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
index f94062788f..1ebcabf417 100644
--- a/src/test/java/roomescape/integration/AdminIntegrationTest.java
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -85,7 +85,7 @@ void adminReservationPageWork() {
         RestAssured.given().log().all()
                 .when().delete("/reservations/1")
                 .then().log().all()
-                .statusCode(200);
+                .statusCode(204);
 
         RestAssured.given().log().all()
                 .when().get("/reservations")
@@ -115,7 +115,7 @@ void adminReservationPageWorkWithDB() {
         RestAssured.given().log().all()
                 .when().delete("/reservations/1")
                 .then().log().all()
-                .statusCode(200);
+                .statusCode(204);
 
         Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
         Assertions.assertThat(countAfterDelete).isEqualTo(0);
@@ -143,6 +143,6 @@ void reservationTimePageWork() {
         RestAssured.given().log().all()
                 .when().delete("/times/2")
                 .then().log().all()
-                .statusCode(200);
+                .statusCode(204);
     }
 }

From bae62c990daec11c1704c4d67c88a77c914d45b3 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 15:41:16 +0900
Subject: [PATCH 06/75] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?=
 =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 src/main/resources/application.properties | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index e69de29bb2..63c01678ab 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+spring.h2.console.enabled=true
+spring.datasource.url=jdbc:h2:mem:product

From fc59282994985bc1251e2eb4cd2c755d88a4fe87 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 15:46:52 +0900
Subject: [PATCH 07/75] =?UTF-8?q?fix:=20hh:MM=20=ED=98=95=EC=8B=9D?=
 =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=EC=8B=9C=EA=B0=84=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20=EC=9A=94=EC=B2=AD=EC=8B=9C=20=EC=98=88=EC=99=B8=20?=
 =?UTF-8?q?=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                      |  2 +-
 .../roomescape/config/TimeFormatterConfig.java |  6 ++++--
 .../controller/RoomescapeExceptionHandler.java | 18 ++++++++++++++++++
 .../java/roomescape/dto/ErrorResponse.java     |  4 ++++
 4 files changed, 27 insertions(+), 3 deletions(-)
 create mode 100644 src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
 create mode 100644 src/main/java/roomescape/dto/ErrorResponse.java

diff --git a/README.md b/README.md
index 1c39527256..725e1478e2 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 - [x] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정
 - [ ] 예약 시간에 대한 제약 조건 추가
     - [ ] 중복된 예약 시간 생성 요청 시 에러
-    - [ ] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
+    - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
     - [ ] 예약이 있는 예약 시간을 삭제 요청 시 에러
     - [ ] 존재하지 않는 시간을 삭제 요청 시 에러
 - [ ] 예약에 대한 제약 조건 추가
diff --git a/src/main/java/roomescape/config/TimeFormatterConfig.java b/src/main/java/roomescape/config/TimeFormatterConfig.java
index 35c20b11b1..3cf3aa6c9c 100644
--- a/src/main/java/roomescape/config/TimeFormatterConfig.java
+++ b/src/main/java/roomescape/config/TimeFormatterConfig.java
@@ -1,5 +1,6 @@
 package roomescape.config;
 
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
 import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
 import java.time.format.DateTimeFormatter;
 import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
@@ -8,10 +9,11 @@
 
 @Configuration
 public class TimeFormatterConfig {
-    private static final String TIME_FORMAT = "HH:mm";
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
 
     @Bean
     public Jackson2ObjectMapperBuilderCustomizer localTimeSerializerCustomizer() {
-        return builder -> builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
+        return builder -> builder.serializers(new LocalTimeSerializer(FORMATTER))
+                .deserializers(new LocalTimeDeserializer(FORMATTER));
     }
 }
diff --git a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java b/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
new file mode 100644
index 0000000000..da293fb62f
--- /dev/null
+++ b/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
@@ -0,0 +1,18 @@
+package roomescape.controller;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import roomescape.dto.ErrorResponse;
+
+@ControllerAdvice
+public class RoomescapeExceptionHandler {
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<ErrorResponse> handle(HttpMessageNotReadableException e) {
+        e.printStackTrace();
+        return ResponseEntity.badRequest()
+                .body(new ErrorResponse("입력값이 잘못되었습니다."));
+    }
+}
diff --git a/src/main/java/roomescape/dto/ErrorResponse.java b/src/main/java/roomescape/dto/ErrorResponse.java
new file mode 100644
index 0000000000..a218a73952
--- /dev/null
+++ b/src/main/java/roomescape/dto/ErrorResponse.java
@@ -0,0 +1,4 @@
+package roomescape.dto;
+
+public record ErrorResponse(String message) {
+}

From 65733d0b65cc6a53b8b295bae8821d0fca43487f Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 16:15:37 +0900
Subject: [PATCH 08/75] =?UTF-8?q?refactor=20:=20Repository=20=EA=B0=80=20D?=
 =?UTF-8?q?TO=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A?=
 =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 .../java/roomescape/domain/Reservation.java   |  8 +++--
 .../JdbcTemplateReservationRepository.java    | 13 ++++----
 ...JdbcTemplateReservationTimeRepository.java | 11 +++----
 .../repository/ReservationRepository.java     |  3 +-
 .../repository/ReservationTimeRepository.java |  3 +-
 .../service/ReservationService.java           |  6 +++-
 .../service/ReservationTimeService.java       |  3 +-
 .../CollectionReservationRepository.java      | 32 +++++++------------
 .../CollectionReservationTimeRepository.java  | 10 +++---
 ...JdbcTemplateReservationRepositoryTest.java | 12 ++++---
 ...TemplateReservationTimeRepositoryTest.java |  9 +++---
 11 files changed, 53 insertions(+), 57 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index 004dfd31ac..a64065c4cd 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -11,8 +11,8 @@ public class Reservation implements Comparable<Reservation> {
     private final LocalDate date;
     private final ReservationTime time;
 
-    public Reservation(long id, Reservation reservationBeforeSave) {
-        this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time);
+    public Reservation(String name, LocalDate date, ReservationTime time) {
+        this(null, name, date, time);
     }
 
     public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
@@ -22,6 +22,10 @@ public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
         this.time = time;
     }
 
+    public Reservation(long id, Reservation reservationBeforeSave) {
+        this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time);
+    }
+
     @Override
     public int compareTo(Reservation other) {
         LocalDateTime dateTime = LocalDateTime.of(date, time.getStartAt());
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index 7884d19fdc..31eef64900 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -11,7 +11,6 @@
 import org.springframework.stereotype.Repository;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
-import roomescape.dto.ReservationRequest;
 
 @Repository
 public class JdbcTemplateReservationRepository implements ReservationRepository {
@@ -22,23 +21,23 @@ public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) {
     }
 
     @Override
-    public Reservation save(ReservationRequest reservationRequest) {
-        ReservationTime reservationTime = findReservationTime(reservationRequest);
-        Reservation reservation = new Reservation(null, reservationRequest.name(), reservationRequest.date(),
+    public Reservation save(Reservation reservation) {
+        ReservationTime reservationTime = findReservationTime(reservation.getReservationTime().getId());
+        Reservation beforeSaved = new Reservation(null, reservation.getName(), reservation.getDate(),
                 reservationTime);
         KeyHolder keyHolder = new GeneratedKeyHolder();
-        save(reservation, keyHolder);
+        save(beforeSaved, keyHolder);
         long id = keyHolder.getKey().longValue();
         return new Reservation(id, reservation);
     }
 
-    private ReservationTime findReservationTime(ReservationRequest reservationRequest) {
+    private ReservationTime findReservationTime(long timeId) {
         String reservationTimeSelectSql = "select * from reservation_time where id = ?";
         return jdbcTemplate.queryForObject(reservationTimeSelectSql, (rs, rowNum) -> {
             long id = rs.getLong(1);
             LocalTime startAt = rs.getTime(2).toLocalTime();
             return new ReservationTime(id, startAt);
-        }, reservationRequest.timeId());
+        }, timeId);
     }
 
     private void save(Reservation reservation, KeyHolder keyHolder) {
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
index 639db36eaa..2e7950a460 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -9,7 +9,6 @@
 import org.springframework.jdbc.support.KeyHolder;
 import org.springframework.stereotype.Repository;
 import roomescape.domain.ReservationTime;
-import roomescape.dto.ReservationTimeRequest;
 
 @Repository
 public class JdbcTemplateReservationTimeRepository implements ReservationTimeRepository {
@@ -20,17 +19,17 @@ public JdbcTemplateReservationTimeRepository(JdbcTemplate jdbcTemplate) {
     }
 
     @Override
-    public ReservationTime save(ReservationTimeRequest reservationTimeRequest) {
+    public ReservationTime save(ReservationTime reservationTime) {
         KeyHolder keyHolder = new GeneratedKeyHolder();
-        save(reservationTimeRequest, keyHolder);
-        return new ReservationTime(keyHolder.getKey().longValue(), reservationTimeRequest.startAt());
+        save(reservationTime, keyHolder);
+        return new ReservationTime(keyHolder.getKey().longValue(), reservationTime.getStartAt());
     }
 
-    private void save(ReservationTimeRequest reservationTimeRequest, KeyHolder keyHolder) {
+    private void save(ReservationTime reservationTime, KeyHolder keyHolder) {
         jdbcTemplate.update(con -> {
             PreparedStatement pstmt = con.prepareStatement("insert into reservation_time(start_at) values ( ? )",
                     new String[]{"id"});
-            pstmt.setTime(1, Time.valueOf(reservationTimeRequest.startAt()));
+            pstmt.setTime(1, Time.valueOf(reservationTime.getStartAt()));
             return pstmt;
         }, keyHolder);
     }
diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java
index 79c62ca32f..1ac0fbd917 100644
--- a/src/main/java/roomescape/repository/ReservationRepository.java
+++ b/src/main/java/roomescape/repository/ReservationRepository.java
@@ -2,10 +2,9 @@
 
 import java.util.List;
 import roomescape.domain.Reservation;
-import roomescape.dto.ReservationRequest;
 
 public interface ReservationRepository {
-    Reservation save(ReservationRequest reservationRequest);
+    Reservation save(Reservation reservation);
 
     List<Reservation> findAll();
 
diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java
index 5f6e144eea..ac26358500 100644
--- a/src/main/java/roomescape/repository/ReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java
@@ -2,10 +2,9 @@
 
 import java.util.List;
 import roomescape.domain.ReservationTime;
-import roomescape.dto.ReservationTimeRequest;
 
 public interface ReservationTimeRepository {
-    ReservationTime save(ReservationTimeRequest reservationTimeRequest);
+    ReservationTime save(ReservationTime reservationTime);
 
     List<ReservationTime> findAll();
 
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index de6d8d5288..1c0e54c4b5 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -18,7 +18,11 @@ public ReservationService(ReservationRepository reservationRepository) {
     }
 
     public ReservationResponse save(ReservationRequest reservationRequest) {
-        Reservation saved = reservationRepository.save(reservationRequest);
+        Reservation saved = reservationRepository.save(new Reservation(
+                reservationRequest.name(),
+                reservationRequest.date(),
+                new ReservationTime(reservationRequest.timeId(), null)
+        ));
         return toResponse(saved);
     }
 
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 908a747d0b..e6548bb1d7 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -16,7 +16,8 @@ public ReservationTimeService(ReservationTimeRepository reservationTimeRepositor
     }
 
     public ReservationTimeResponse save(ReservationTimeRequest reservationTimeRequest) {
-        ReservationTime saved = reservationTimeRepository.save(reservationTimeRequest);
+        ReservationTime reservationTime = new ReservationTime(reservationTimeRequest.startAt());
+        ReservationTime saved = reservationTimeRepository.save(reservationTime);
         return toResponse(saved);
     }
 
diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java
index 087cc9de04..ef0eac28b5 100644
--- a/src/test/java/roomescape/repository/CollectionReservationRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java
@@ -1,6 +1,5 @@
 package roomescape.repository;
 
-import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
@@ -30,29 +29,22 @@ public CollectionReservationRepository(List<Reservation> reservations,
         this(reservations, new AtomicLong(0), timeRepository);
     }
 
-    @Override
-    public Reservation save(ReservationRequest reservationRequest) {
-        Reservation reservation = fromRequest(reservationRequest);
-        reservations.add(reservation);
-        return reservation;
-    }
-
-    private Reservation fromRequest(ReservationRequest reservationRequest) {
-        long id = atomicLong.incrementAndGet();
-
-        String name = reservationRequest.name();
-        LocalDate date = reservationRequest.date();
-        ReservationTime reservationTime = timeRepository.findAll().stream()
-                .filter(sameId(reservationRequest))
-                .findAny()
-                .orElseThrow();
-        return new Reservation(id, name, date, reservationTime);
-    }
-
     private static Predicate<ReservationTime> sameId(ReservationRequest reservationRequest) {
         return reservationTime -> reservationTime.getId() == reservationRequest.timeId();
     }
 
+    @Override
+    public Reservation save(Reservation reservation) {
+        ReservationTime findTime = timeRepository.findAll().stream()
+                .filter(reservationTime -> reservationTime.getId() == reservation.getReservationTime().getId())
+                .findFirst()
+                .get();
+        Reservation saved = new Reservation(atomicLong.incrementAndGet(), reservation.getName(), reservation.getDate(),
+                findTime);
+        reservations.add(saved);
+        return saved;
+    }
+
     @Override
     public List<Reservation> findAll() {
         return reservations.stream()
diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index b6f22fe988..6b90de607c 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -4,7 +4,6 @@
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
 import roomescape.domain.ReservationTime;
-import roomescape.dto.ReservationTimeRequest;
 
 public class CollectionReservationTimeRepository implements ReservationTimeRepository {
     private final List<ReservationTime> reservationTimes;
@@ -24,11 +23,10 @@ public CollectionReservationTimeRepository(List<ReservationTime> reservationTime
     }
 
     @Override
-    public ReservationTime save(ReservationTimeRequest reservationTimeRequest) {
-        ReservationTime reservationTime = new ReservationTime(atomicLong.incrementAndGet(),
-                reservationTimeRequest.startAt());
-        reservationTimes.add(reservationTime);
-        return reservationTime;
+    public ReservationTime save(ReservationTime reservationTime) {
+        ReservationTime saved = new ReservationTime(atomicLong.incrementAndGet(), reservationTime.getStartAt());
+        reservationTimes.add(saved);
+        return saved;
     }
 
     @Override
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
index c2acc8994f..ddb8faa35b 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
@@ -1,6 +1,7 @@
 package roomescape.repository;
 
 import java.time.LocalDate;
+import java.time.LocalTime;
 import java.util.List;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
@@ -10,10 +11,11 @@
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.jdbc.core.JdbcTemplate;
 import roomescape.domain.Reservation;
-import roomescape.dto.ReservationRequest;
+import roomescape.domain.ReservationTime;
 
 @SpringBootTest
 class JdbcTemplateReservationRepositoryTest {
+    private static final ReservationTime DEFAULT_TIME = new ReservationTime(1L, LocalTime.of(11, 56));
     @Autowired
     private ReservationRepository reservationRepository;
     @Autowired
@@ -32,7 +34,7 @@ void init() {
     @DisplayName("Reservation 을 잘 저장하는지 확인한다.")
     void save() {
         var beforeSave = reservationRepository.findAll();
-        Reservation saved = reservationRepository.save(new ReservationRequest(LocalDate.now(), "test", 1));
+        Reservation saved = reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME));
         var afterSave = reservationRepository.findAll();
 
         Assertions.assertThat(afterSave)
@@ -44,8 +46,8 @@ void save() {
     @DisplayName("Reservation 을 잘 조회하는지 확인한다.")
     void findAll() {
         List<Reservation> beforeSave = reservationRepository.findAll();
-        reservationRepository.save(new ReservationRequest(LocalDate.now(), "test", 1));
-        reservationRepository.save(new ReservationRequest(LocalDate.now(), "test2", 1));
+        reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME));
+        reservationRepository.save(new Reservation("test2", LocalDate.now(), DEFAULT_TIME));
 
         List<Reservation> afterSave = reservationRepository.findAll();
         Assertions.assertThat(afterSave.size())
@@ -56,7 +58,7 @@ void findAll() {
     @DisplayName("Reservation 을 잘 지우는지 확인한다.")
     void delete() {
         List<Reservation> beforeSaveAndDelete = reservationRepository.findAll();
-        reservationRepository.save(new ReservationRequest(LocalDate.now(), "test", 1));
+        reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME));
 
         reservationRepository.delete(1L);
 
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
index 5bc5cd67f2..71d364044d 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
@@ -10,7 +10,6 @@
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.jdbc.core.JdbcTemplate;
 import roomescape.domain.ReservationTime;
-import roomescape.dto.ReservationTimeRequest;
 
 @SpringBootTest
 class JdbcTemplateReservationTimeRepositoryTest {
@@ -31,7 +30,7 @@ void init() {
     @DisplayName("ReservationTime 을 잘 저장하는지 확인한다.")
     void save() {
         var beforeSave = reservationTimeRepository.findAll().stream().map(ReservationTime::getId).toList();
-        ReservationTime saved = reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+        ReservationTime saved = reservationTimeRepository.save(new ReservationTime(LocalTime.now()));
         var afterSave = reservationTimeRepository.findAll().stream().map(ReservationTime::getId).toList();
 
         Assertions.assertThat(afterSave)
@@ -43,8 +42,8 @@ void save() {
     @DisplayName("ReservationTime 을 잘 조회하는지 확인한다.")
     void findAll() {
         List<ReservationTime> beforeSave = reservationTimeRepository.findAll();
-        reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
-        reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.now()));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.now()));
 
         List<ReservationTime> afterSave = reservationTimeRepository.findAll();
 
@@ -56,7 +55,7 @@ void findAll() {
     @DisplayName("ReservationTime 을 잘 지우하는지 확인한다.")
     void delete() {
         List<ReservationTime> beforeSaveAndDelete = reservationTimeRepository.findAll();
-        reservationTimeRepository.save(new ReservationTimeRequest(LocalTime.now()));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.now()));
 
         reservationTimeRepository.delete(1L);
 

From 1bbadab2c025afcae469f8d7b4eac5a4d3c8a443 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 17:04:24 +0900
Subject: [PATCH 09/75] =?UTF-8?q?feat=20:=20ReservationTime=20=EC=9D=98=20?=
 =?UTF-8?q?startAt=20=EC=97=90=20=EB=8C=80=ED=95=9C=20null=20=EA=B2=80?=
 =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A1=B4=EC=9E=AC?=
 =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=8B=9C=EA=B0=84?=
 =?UTF-8?q?=EC=97=90=20=EC=98=88=EC=95=BD=20=EC=83=9D=EC=84=B1=20=EC=8B=9C?=
 =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EB=B0=9C=EC=83=9D=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                     |  2 +-
 .../RoomescapeExceptionHandler.java           |  8 ++++++
 .../roomescape/domain/ReservationTime.java    |  4 +++
 ...JdbcTemplateReservationTimeRepository.java | 25 +++++++++++++------
 .../repository/ReservationTimeRepository.java |  3 +++
 .../service/ReservationService.java           | 13 ++++++++--
 .../CollectionReservationTimeRepository.java  |  8 ++++++
 7 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index 725e1478e2..75a938003c 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
     - [ ] 존재하지 않는 시간을 삭제 요청 시 에러
 - [ ] 예약에 대한 제약 조건 추가
     - [ ] 동일한 날짜와 시간에 예약 생성 요청 시 에러
-    - [ ] 존재하지 않는 시간에 예약 생성 요청 시 에러
+    - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
     - [ ] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [ ] 존재하지 않는 예약을 삭제 요청 시 에러
     - [ ] 지나간 날짜와 시간의 예약 요청 시 에러
diff --git a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java b/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
index da293fb62f..e9a2f23984 100644
--- a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
+++ b/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
@@ -15,4 +15,12 @@ public ResponseEntity<ErrorResponse> handle(HttpMessageNotReadableException e) {
         return ResponseEntity.badRequest()
                 .body(new ErrorResponse("입력값이 잘못되었습니다."));
     }
+
+    //TODO : 커스템 에러로 처리하도록 변경
+    @ExceptionHandler(IllegalArgumentException.class)
+    public ResponseEntity<ErrorResponse> handle(IllegalArgumentException e) {
+        e.printStackTrace();
+        return ResponseEntity.badRequest().
+                body(new ErrorResponse(e.getMessage()));
+    }
 }
diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index 8b54f99f81..63f0c19ef3 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -11,7 +11,11 @@ public ReservationTime(LocalTime startAt) {
         this(null, startAt);
     }
 
+    //TODO 테스트 추가
     public ReservationTime(Long id, LocalTime startAt) {
+        if (startAt == null) {
+            throw new IllegalArgumentException("시간은 없을 수 없습니다.");
+        }
         this.id = id;
         this.startAt = startAt;
     }
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
index 2e7950a460..2f2a3251b3 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -4,6 +4,7 @@
 import java.sql.Time;
 import java.time.LocalTime;
 import java.util.List;
+import java.util.Optional;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.support.GeneratedKeyHolder;
 import org.springframework.jdbc.support.KeyHolder;
@@ -25,13 +26,14 @@ public ReservationTime save(ReservationTime reservationTime) {
         return new ReservationTime(keyHolder.getKey().longValue(), reservationTime.getStartAt());
     }
 
-    private void save(ReservationTime reservationTime, KeyHolder keyHolder) {
-        jdbcTemplate.update(con -> {
-            PreparedStatement pstmt = con.prepareStatement("insert into reservation_time(start_at) values ( ? )",
-                    new String[]{"id"});
-            pstmt.setTime(1, Time.valueOf(reservationTime.getStartAt()));
-            return pstmt;
-        }, keyHolder);
+    @Override
+    public Optional<ReservationTime> findById(long id) {
+        List<ReservationTime> times = jdbcTemplate.query("select start_at from reservation_time where id = ?",
+                (rs, rowNum) -> {
+                    LocalTime time = rs.getTime(1).toLocalTime();
+                    return new ReservationTime(id, time);
+                }, id);
+        return times.stream().findFirst();
     }
 
     @Override
@@ -47,4 +49,13 @@ public List<ReservationTime> findAll() {
     public void delete(long id) {
         jdbcTemplate.update("delete from reservation_time where id = ?", id);
     }
+
+    private void save(ReservationTime reservationTime, KeyHolder keyHolder) {
+        jdbcTemplate.update(con -> {
+            PreparedStatement pstmt = con.prepareStatement("insert into reservation_time(start_at) values ( ? )",
+                    new String[]{"id"});
+            pstmt.setTime(1, Time.valueOf(reservationTime.getStartAt()));
+            return pstmt;
+        }, keyHolder);
+    }
 }
diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java
index ac26358500..5c8b92d09c 100644
--- a/src/main/java/roomescape/repository/ReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java
@@ -1,11 +1,14 @@
 package roomescape.repository;
 
 import java.util.List;
+import java.util.Optional;
 import roomescape.domain.ReservationTime;
 
 public interface ReservationTimeRepository {
     ReservationTime save(ReservationTime reservationTime);
 
+    Optional<ReservationTime> findById(long id);
+
     List<ReservationTime> findAll();
 
     void delete(long id);
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 1c0e54c4b5..764a88b110 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,6 +1,7 @@
 package roomescape.service;
 
 import java.util.List;
+import java.util.Optional;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
@@ -8,20 +9,28 @@
 import roomescape.dto.ReservationResponse;
 import roomescape.dto.ReservationTimeResponse;
 import roomescape.repository.ReservationRepository;
+import roomescape.repository.ReservationTimeRepository;
 
 @Service
 public class ReservationService {
     private final ReservationRepository reservationRepository;
+    private final ReservationTimeRepository reservationTimeRepository;
 
-    public ReservationService(ReservationRepository reservationRepository) {
+    public ReservationService(ReservationRepository reservationRepository,
+                              ReservationTimeRepository reservationTimeRepository) {
         this.reservationRepository = reservationRepository;
+        this.reservationTimeRepository = reservationTimeRepository;
     }
 
     public ReservationResponse save(ReservationRequest reservationRequest) {
+        //TODO 변수명
+        Optional<ReservationTime> reservationTime = reservationTimeRepository.findById(reservationRequest.timeId());
+
         Reservation saved = reservationRepository.save(new Reservation(
                 reservationRequest.name(),
                 reservationRequest.date(),
-                new ReservationTime(reservationRequest.timeId(), null)
+                //TODO : 커스텀 예외 사용할지 고민해보기
+                reservationTime.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 시간입니다."))
         ));
         return toResponse(saved);
     }
diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index 6b90de607c..77f1d2d834 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -2,6 +2,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 import roomescape.domain.ReservationTime;
 
@@ -29,6 +30,13 @@ public ReservationTime save(ReservationTime reservationTime) {
         return saved;
     }
 
+    @Override
+    public Optional<ReservationTime> findById(long id) {
+        return reservationTimes.stream()
+                .filter(reservationTime -> reservationTime.getId() == id) //Todo 메서드로 바꾸기
+                .findFirst();
+    }
+
     @Override
     public List<ReservationTime> findAll() {
         return List.copyOf(reservationTimes);

From a14d3903b18783158f36254de8fd6d708a594ad9 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 17:23:58 +0900
Subject: [PATCH 10/75] =?UTF-8?q?feat=20:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?=
 =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=8B=9C=EA=B0=84=20=EC=83=9D=EC=84=B1=20?=
 =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                     |  2 +-
 ...JdbcTemplateReservationTimeRepository.java |  7 +++++
 .../repository/ReservationTimeRepository.java |  4 +++
 .../service/ReservationTimeService.java       |  3 +++
 .../controller/ReservationControllerTest.java |  8 +++---
 .../CollectionReservationTimeRepository.java  |  7 +++++
 .../service/ReservationTimeServiceTest.java   | 26 +++++++++++++++++++
 7 files changed, 52 insertions(+), 5 deletions(-)
 create mode 100644 src/test/java/roomescape/service/ReservationTimeServiceTest.java

diff --git a/README.md b/README.md
index 75a938003c..2735894936 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 - [x] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정
 - [ ] 예약 시간에 대한 제약 조건 추가
-    - [ ] 중복된 예약 시간 생성 요청 시 에러
+    - [x] 중복된 예약 시간 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
     - [ ] 예약이 있는 예약 시간을 삭제 요청 시 에러
     - [ ] 존재하지 않는 시간을 삭제 요청 시 에러
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
index 2f2a3251b3..9b6685bb52 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -26,6 +26,13 @@ public ReservationTime save(ReservationTime reservationTime) {
         return new ReservationTime(keyHolder.getKey().longValue(), reservationTime.getStartAt());
     }
 
+    @Override
+    public boolean existsByStartAt(LocalTime startAt) {
+        return jdbcTemplate.queryForObject(
+                "select exists(select 1 from RESERVATION_TIME where START_AT = ?)",
+                Boolean.class, startAt);
+    }
+
     @Override
     public Optional<ReservationTime> findById(long id) {
         List<ReservationTime> times = jdbcTemplate.query("select start_at from reservation_time where id = ?",
diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java
index 5c8b92d09c..f30eeb9c60 100644
--- a/src/main/java/roomescape/repository/ReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java
@@ -1,5 +1,6 @@
 package roomescape.repository;
 
+import java.time.LocalTime;
 import java.util.List;
 import java.util.Optional;
 import roomescape.domain.ReservationTime;
@@ -7,6 +8,9 @@
 public interface ReservationTimeRepository {
     ReservationTime save(ReservationTime reservationTime);
 
+    //Todo 메서드명 고민
+    boolean existsByStartAt(LocalTime startAt);
+
     Optional<ReservationTime> findById(long id);
 
     List<ReservationTime> findAll();
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index e6548bb1d7..1d20629e9c 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -16,6 +16,9 @@ public ReservationTimeService(ReservationTimeRepository reservationTimeRepositor
     }
 
     public ReservationTimeResponse save(ReservationTimeRequest reservationTimeRequest) {
+        if (reservationTimeRepository.existsByStartAt(reservationTimeRequest.startAt())) {
+            throw new IllegalArgumentException("중복된 시간은 생성할 수 없습니다.");
+        }
         ReservationTime reservationTime = new ReservationTime(reservationTimeRequest.startAt());
         ReservationTime saved = reservationTimeRepository.save(reservationTime);
         return toResponse(saved);
diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index 37ddb85fbb..b358e198cc 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -30,7 +30,7 @@ class ReservationControllerTest {
     void saveReservation() {
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
         ReservationController reservationController = new ReservationController(reservationService);
         LocalDate date = LocalDate.now();
 
@@ -51,7 +51,7 @@ void saveReservation() {
     void findAllReservations() {
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
         ReservationController reservationController = new ReservationController(reservationService);
         List<ReservationResponse> allReservations = reservationController.findAllReservations();
 
@@ -66,7 +66,7 @@ void delete() {
                 new Reservation(1L, "폴라", LocalDate.now(), new ReservationTime(LocalTime.now())));
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 new ArrayList<>(reservations), timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
         ReservationController reservationController = new ReservationController(reservationService);
 
         reservationController.delete(1L);
@@ -81,7 +81,7 @@ void delete() {
     void checkRepositoryDependency() {
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
         ReservationController reservationController = new ReservationController(reservationService);
 
         boolean isRepositoryInjected = false;
diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index 77f1d2d834..fc8901ecf4 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -1,5 +1,6 @@
 package roomescape.repository;
 
+import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -30,6 +31,12 @@ public ReservationTime save(ReservationTime reservationTime) {
         return saved;
     }
 
+    @Override
+    public boolean existsByStartAt(LocalTime startAt) {
+        return reservationTimes.stream()
+                .anyMatch(reservationTime -> startAt.equals(reservationTime.getStartAt()));
+    }
+
     @Override
     public Optional<ReservationTime> findById(long id) {
         return reservationTimes.stream()
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
new file mode 100644
index 0000000000..7e3ab8de9f
--- /dev/null
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -0,0 +1,26 @@
+package roomescape.service;
+
+import java.time.LocalTime;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import roomescape.dto.ReservationTimeRequest;
+import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.ReservationTimeRepository;
+
+class ReservationTimeServiceTest {
+
+    @Test
+    @DisplayName("중복된 시간은 생성할 수 없는지 검증")
+    void saveFailCauseDuplicate() {
+        ReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        ReservationTimeRequest reservationTimeRequest = new ReservationTimeRequest(LocalTime.of(10, 0));
+        reservationTimeService.save(reservationTimeRequest);
+
+        Assertions.assertThatThrownBy(() -> reservationTimeService.save(reservationTimeRequest))
+                .isInstanceOf(IllegalArgumentException.class)
+                //TODO 커스텀 에러
+                .hasMessage("중복된 시간은 생성할 수 없습니다.");
+    }
+}

From 2da2b7ac36e52a982bf4971317f53580804a2376 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 17:33:33 +0900
Subject: [PATCH 11/75] =?UTF-8?q?feat=20:=20=EC=98=88=EC=95=BD=EC=9D=B4=20?=
 =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=98=88=EC=95=BD=20=EC=8B=9C=EA=B0=84?=
 =?UTF-8?q?=EC=9D=84=20=EC=82=AD=EC=A0=9C=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C?=
 =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                     |  2 +-
 .../RoomescapeExceptionHandler.java           |  4 ++--
 .../service/ReservationTimeService.java       | 16 +++++++++++++-
 .../ReservationTimeControllerTest.java        | 21 +++++++++++++++----
 .../service/ReservationTimeServiceTest.java   |  9 +++++---
 5 files changed, 41 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index 2735894936..c63c9ee357 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 - [ ] 예약 시간에 대한 제약 조건 추가
     - [x] 중복된 예약 시간 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
-    - [ ] 예약이 있는 예약 시간을 삭제 요청 시 에러
+    - [x] 예약이 있는 예약 시간을 삭제 요청 시 에러
     - [ ] 존재하지 않는 시간을 삭제 요청 시 에러
 - [ ] 예약에 대한 제약 조건 추가
     - [ ] 동일한 날짜와 시간에 예약 생성 요청 시 에러
diff --git a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java b/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
index e9a2f23984..e7cc8497d2 100644
--- a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
+++ b/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
@@ -17,8 +17,8 @@ public ResponseEntity<ErrorResponse> handle(HttpMessageNotReadableException e) {
     }
 
     //TODO : 커스템 에러로 처리하도록 변경
-    @ExceptionHandler(IllegalArgumentException.class)
-    public ResponseEntity<ErrorResponse> handle(IllegalArgumentException e) {
+    @ExceptionHandler(RuntimeException.class)
+    public ResponseEntity<ErrorResponse> handle(RuntimeException e) {
         e.printStackTrace();
         return ResponseEntity.badRequest().
                 body(new ErrorResponse(e.getMessage()));
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 1d20629e9c..09d12908a3 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -2,16 +2,21 @@
 
 import java.util.List;
 import org.springframework.stereotype.Service;
+import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
 import roomescape.dto.ReservationTimeRequest;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.repository.ReservationRepository;
 import roomescape.repository.ReservationTimeRepository;
 
 @Service
 public class ReservationTimeService {
+    private final ReservationRepository reservationRepository;
     private final ReservationTimeRepository reservationTimeRepository;
 
-    public ReservationTimeService(ReservationTimeRepository reservationTimeRepository) {
+    public ReservationTimeService(ReservationRepository reservationRepository,
+                                  ReservationTimeRepository reservationTimeRepository) {
+        this.reservationRepository = reservationRepository;
         this.reservationTimeRepository = reservationTimeRepository;
     }
 
@@ -34,7 +39,16 @@ public List<ReservationTimeResponse> findAll() {
                 .toList();
     }
 
+    //TODO : 테스트 생성
     public void delete(long id) {
+        //todo SQL로 구현
+        List<Reservation> reservations = reservationRepository.findAll();
+        //TODO : 지역변수 네이밍 고민
+        boolean invalidDelete = reservations.stream()
+                .anyMatch(reservation -> reservation.getReservationTime().getId() == id);
+        if (invalidDelete) {
+            throw new IllegalStateException("예약이 존재하는 시간을 지울 수 없습니다.");
+        }
         reservationTimeRepository.delete(id);
     }
 }
diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
index d9b50035ca..7726d9e2d5 100644
--- a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
@@ -10,6 +10,7 @@
 import roomescape.domain.ReservationTime;
 import roomescape.dto.ReservationTimeRequest;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
 import roomescape.service.ReservationTimeService;
 
@@ -18,7 +19,10 @@ class ReservationTimeControllerTest {
     @DisplayName("시간을 잘 저장하는지 확인한다.")
     void save() {
         CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
+                reservationTimeRepository);
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
+                reservationTimeRepository);
         ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
         LocalTime time = LocalTime.now();
 
@@ -33,7 +37,10 @@ void save() {
     @DisplayName("시간을 잘 불러오는지 확인한다.")
     void findAll() {
         CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
+                reservationTimeRepository);
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
+                reservationTimeRepository);
         ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
         List<ReservationTimeResponse> reservationTimeResponses = reservationTimeController.findAll();
 
@@ -47,7 +54,10 @@ void delete() {
         CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository(
                 new ArrayList<>(List.of(new ReservationTime(1L, LocalTime.now())))
         );
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
+                reservationTimeRepository);
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
+                reservationTimeRepository);
         ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
 
         reservationTimeController.delete(1);
@@ -61,7 +71,10 @@ void delete() {
     @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.")
     void checkRepositoryDependency() {
         CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
+                reservationTimeRepository);
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
+                reservationTimeRepository);
         ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
 
         boolean isRepositoryInjected = false;
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index 7e3ab8de9f..05bfff4ced 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -5,16 +5,19 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import roomescape.dto.ReservationTimeRequest;
+import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
-import roomescape.repository.ReservationTimeRepository;
 
 class ReservationTimeServiceTest {
 
     @Test
     @DisplayName("중복된 시간은 생성할 수 없는지 검증")
     void saveFailCauseDuplicate() {
-        ReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationTimeRepository);
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
+                reservationTimeRepository);
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
+                reservationTimeRepository);
         ReservationTimeRequest reservationTimeRequest = new ReservationTimeRequest(LocalTime.of(10, 0));
         reservationTimeService.save(reservationTimeRequest);
 

From 4f222f4acc92cdf836401c5eef5efe9945bef66d Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 17:41:27 +0900
Subject: [PATCH 12/75] =?UTF-8?q?docs=20:=20=EC=A1=B4=EC=9E=AC=ED=95=98?=
 =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9E=90=EC=9B=90=EC=9D=84=20?=
 =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20?=
 =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 어차피 자원이 존재하지 않는 것은 동일하므로 문제가 없다고 결정

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/README.md b/README.md
index c63c9ee357..6c7e7c2133 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,14 @@
 # 요구사항 문서
 
 - [x] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정
-- [ ] 예약 시간에 대한 제약 조건 추가
+- [x] 예약 시간에 대한 제약 조건 추가
     - [x] 중복된 예약 시간 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
     - [x] 예약이 있는 예약 시간을 삭제 요청 시 에러
-    - [ ] 존재하지 않는 시간을 삭제 요청 시 에러
 - [ ] 예약에 대한 제약 조건 추가
     - [ ] 동일한 날짜와 시간에 예약 생성 요청 시 에러
     - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
     - [ ] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
-    - [ ] 존재하지 않는 예약을 삭제 요청 시 에러
     - [ ] 지나간 날짜와 시간의 예약 요청 시 에러
 
 # API 명세

From 6a4ebb7c19e0afe91ab42966346e410cffd635f3 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 17:49:48 +0900
Subject: [PATCH 13/75] =?UTF-8?q?feat:=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?=
 =?UTF-8?q?=EB=82=A0=EC=A7=9C=EC=99=80=20=EC=8B=9C=EA=B0=84=EC=97=90=20?=
 =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=83=9D=EC=84=B1=20=EC=9A=94=EC=B2=AD=20?=
 =?UTF-8?q?=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                     |  2 +-
 .../service/ReservationService.java           | 21 +++++++++++++++++--
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 6c7e7c2133..dab9f638df 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
     - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
     - [x] 예약이 있는 예약 시간을 삭제 요청 시 에러
 - [ ] 예약에 대한 제약 조건 추가
-    - [ ] 동일한 날짜와 시간에 예약 생성 요청 시 에러
+    - [x] 동일한 날짜와 시간에 예약 생성 요청 시 에러
     - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
     - [ ] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [ ] 지나간 날짜와 시간의 예약 요청 시 에러
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 764a88b110..5757bd910f 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,5 +1,8 @@
 package roomescape.service;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.util.List;
 import java.util.Optional;
 import org.springframework.stereotype.Service;
@@ -22,19 +25,33 @@ public ReservationService(ReservationRepository reservationRepository,
         this.reservationTimeRepository = reservationTimeRepository;
     }
 
+    //ToDo 테스트 작성
     public ReservationResponse save(ReservationRequest reservationRequest) {
         //TODO 변수명
         Optional<ReservationTime> reservationTime = reservationTimeRepository.findById(reservationRequest.timeId());
 
-        Reservation saved = reservationRepository.save(new Reservation(
+        Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
                 reservationRequest.date(),
                 //TODO : 커스텀 예외 사용할지 고민해보기
                 reservationTime.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 시간입니다."))
-        ));
+        );
+        boolean isDuplicate = reservationRepository.findAll()
+                .stream()
+                .anyMatch(reservation -> mapToLocalDateTime(reservation).equals(mapToLocalDateTime(beforeSave)));
+        if (isDuplicate) {
+            throw new IllegalArgumentException("동일한 날짜와 시간에 이미 예약이 존재합니다.");
+        }
+        Reservation saved = reservationRepository.save(beforeSave);
         return toResponse(saved);
     }
 
+    private LocalDateTime mapToLocalDateTime(Reservation reservation) {
+        LocalDate date = reservation.getDate();
+        LocalTime time = reservation.getReservationTime().getStartAt();
+        return LocalDateTime.of(date, time);
+    }
+
     private ReservationResponse toResponse(Reservation reservation) {
         ReservationTime reservationTime = reservation.getReservationTime();
         ReservationTimeResponse reservationTimeResponse = new ReservationTimeResponse(reservationTime.getId(),

From 60e2f8896a2c04d220b47678d4ffab2a1298de90 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 19:13:17 +0900
Subject: [PATCH 14/75] =?UTF-8?q?feat:=20ISO=208601=20=ED=91=9C=EC=A4=80?=
 =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20YYYY-MM-dd=20=ED=8F=AC?=
 =?UTF-8?q?=EB=A7=B7=EC=97=90=20=ED=95=B4=EB=8B=B9=ED=95=98=EC=A7=80=20?=
 =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=82=A0=EC=A7=9C=EA=B0=80=20=ED=8F=AC?=
 =?UTF-8?q?=ED=95=A8=EB=90=9C=20=EC=98=88=EC=95=BD=20=EC=83=9D=EC=84=B1=20?=
 =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                              |  2 +-
 .../java/roomescape/config/TimeFormatterConfig.java    | 10 +++++++---
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index dab9f638df..33ae7832ff 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 - [ ] 예약에 대한 제약 조건 추가
     - [x] 동일한 날짜와 시간에 예약 생성 요청 시 에러
     - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
-    - [ ] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
+    - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [ ] 지나간 날짜와 시간의 예약 요청 시 에러
 
 # API 명세
diff --git a/src/main/java/roomescape/config/TimeFormatterConfig.java b/src/main/java/roomescape/config/TimeFormatterConfig.java
index 3cf3aa6c9c..7b301a6547 100644
--- a/src/main/java/roomescape/config/TimeFormatterConfig.java
+++ b/src/main/java/roomescape/config/TimeFormatterConfig.java
@@ -1,6 +1,8 @@
 package roomescape.config;
 
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
 import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
 import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
 import java.time.format.DateTimeFormatter;
 import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
@@ -9,11 +11,13 @@
 
 @Configuration
 public class TimeFormatterConfig {
-    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
 
     @Bean
     public Jackson2ObjectMapperBuilderCustomizer localTimeSerializerCustomizer() {
-        return builder -> builder.serializers(new LocalTimeSerializer(FORMATTER))
-                .deserializers(new LocalTimeDeserializer(FORMATTER));
+        return builder -> builder.serializers(new LocalTimeSerializer(TIME_FORMATTER),
+                        new LocalDateSerializer(DATE_FORMATTER))
+                .deserializers(new LocalTimeDeserializer(TIME_FORMATTER), new LocalDateDeserializer(DATE_FORMATTER));
     }
 }

From 80b2371fabb048470aa7f34ae74939a71a66eff9 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 19:25:24 +0900
Subject: [PATCH 15/75] =?UTF-8?q?feat:=20=EC=A7=80=EB=82=98=EA=B0=84=20?=
 =?UTF-8?q?=EB=82=A0=EC=A7=9C=EC=99=80=20=EC=8B=9C=EA=B0=84=EC=9D=98=20?=
 =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EC=97=90?=
 =?UTF-8?q?=EB=9F=AC=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                       |  3 ++-
 .../roomescape/service/ReservationService.java  | 17 ++++++++++++++---
 .../controller/ReservationControllerTest.java   |  2 +-
 .../integration/AdminIntegrationTest.java       |  7 +++++--
 4 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 33ae7832ff..ef8f07fe10 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,8 @@
     - [x] 동일한 날짜와 시간에 예약 생성 요청 시 에러
     - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
-    - [ ] 지나간 날짜와 시간의 예약 요청 시 에러
+    - [x] 지나간 날짜와 시간의 예약 요청 시 에러
+    - [ ] 이름이 비어있는 예약 요청 시 에러
 
 # API 명세
 
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 5757bd910f..9327997a0d 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -4,7 +4,6 @@
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.List;
-import java.util.Optional;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
@@ -28,13 +27,14 @@ public ReservationService(ReservationRepository reservationRepository,
     //ToDo 테스트 작성
     public ReservationResponse save(ReservationRequest reservationRequest) {
         //TODO 변수명
-        Optional<ReservationTime> reservationTime = reservationTimeRepository.findById(reservationRequest.timeId());
+        ReservationTime reservationTime = reservationTimeRepository.findById(reservationRequest.timeId())
+                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 시간입니다."));
 
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
                 reservationRequest.date(),
                 //TODO : 커스텀 예외 사용할지 고민해보기
-                reservationTime.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 시간입니다."))
+                reservationTime
         );
         boolean isDuplicate = reservationRepository.findAll()
                 .stream()
@@ -42,10 +42,21 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         if (isDuplicate) {
             throw new IllegalArgumentException("동일한 날짜와 시간에 이미 예약이 존재합니다.");
         }
+
+        //todo 테스트
+        if (isBefore(beforeSave)) {
+            throw new IllegalArgumentException("이미 지난 시간에 예약할 수 없습니다.");
+        }
+
         Reservation saved = reservationRepository.save(beforeSave);
         return toResponse(saved);
     }
 
+    private static boolean isBefore(Reservation beforeSave) {
+        return LocalDateTime.of(beforeSave.getDate(), beforeSave.getTime())
+                .isBefore(LocalDateTime.now());
+    }
+
     private LocalDateTime mapToLocalDateTime(Reservation reservation) {
         LocalDate date = reservation.getDate();
         LocalTime time = reservation.getReservationTime().getStartAt();
diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index b358e198cc..fa257e5e74 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -32,7 +32,7 @@ void saveReservation() {
                 timeRepository);
         ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
         ReservationController reservationController = new ReservationController(reservationService);
-        LocalDate date = LocalDate.now();
+        LocalDate date = LocalDate.now().plusDays(1);
 
         ReservationResponse saveResponse = reservationController.saveReservation(
                         new ReservationRequest(date, "폴라", timeId))
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
index 1ebcabf417..9a525e8289 100644
--- a/src/test/java/roomescape/integration/AdminIntegrationTest.java
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -4,6 +4,8 @@
 
 import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.HashMap;
 import java.util.Map;
 import org.assertj.core.api.Assertions;
@@ -18,6 +20,7 @@
 
 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
 public class AdminIntegrationTest {
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     @LocalServerPort
     private int port;
     @Autowired
@@ -65,7 +68,7 @@ void adminReservationPageWork() {
         System.out.println(integer);
         Map<String, Object> params = new HashMap<>();
         params.put("name", "브라운");
-        params.put("date", "2023-08-05");
+        params.put("date", LocalDate.now().plusDays(1).format(DATE_FORMATTER));
         params.put("timeId", 1);
 
         RestAssured.given().log().all()
@@ -99,7 +102,7 @@ void adminReservationPageWork() {
     void adminReservationPageWorkWithDB() {
         Map<String, Object> params = new HashMap<>();
         params.put("name", "브라운");
-        params.put("date", "2023-08-05");
+        params.put("date", LocalDate.now().plusDays(1).format(DATE_FORMATTER));
         params.put("timeId", 1);
 
         RestAssured.given().log().all()

From a5ca6d4533afef91af8d76c1a10a0158d345aca7 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 19:29:58 +0900
Subject: [PATCH 16/75] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=EC=9D=B4=20?=
 =?UTF-8?q?=EB=B9=84=EC=96=B4=EC=9E=88=EB=8A=94=20=EC=98=88=EC=95=BD=20?=
 =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                        | 4 ++--
 src/main/java/roomescape/domain/Reservation.java | 3 +++
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index ef8f07fe10..f7c1ea8c75 100644
--- a/README.md
+++ b/README.md
@@ -5,12 +5,12 @@
     - [x] 중복된 예약 시간 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
     - [x] 예약이 있는 예약 시간을 삭제 요청 시 에러
-- [ ] 예약에 대한 제약 조건 추가
+- [x] 예약에 대한 제약 조건 추가
     - [x] 동일한 날짜와 시간에 예약 생성 요청 시 에러
     - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [x] 지나간 날짜와 시간의 예약 요청 시 에러
-    - [ ] 이름이 비어있는 예약 요청 시 에러
+    - [x] 이름이 비어있는 예약 요청 시 에러
 
 # API 명세
 
diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index a64065c4cd..c3f7b867e7 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -16,6 +16,9 @@ public Reservation(String name, LocalDate date, ReservationTime time) {
     }
 
     public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
+        if (name == null || name.isBlank()) {
+            throw new IllegalArgumentException("이름은 필수 값입니다.");
+        }
         this.id = id;
         this.name = name;
         this.date = date;

From 36fe37e745cd67874f1a050cd9141d7d685c22e1 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 20:09:48 +0900
Subject: [PATCH 17/75] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=8A=A4=ED=85=80?=
 =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=8F=84=EC=9E=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 .../java/roomescape/domain/Reservation.java   |  5 ++-
 .../roomescape/domain/ReservationTime.java    |  5 ++-
 .../roomescape/exception/ExceptionType.java   | 35 +++++++++++++++++++
 .../exception/RoomescapeException.java        | 20 +++++++++++
 .../RoomescapeExceptionHandler.java           | 18 +++++-----
 .../service/ReservationService.java           | 11 ++++--
 .../service/ReservationTimeService.java       |  8 +++--
 .../service/ReservationTimeServiceTest.java   |  8 +++--
 8 files changed, 92 insertions(+), 18 deletions(-)
 create mode 100644 src/main/java/roomescape/exception/ExceptionType.java
 create mode 100644 src/main/java/roomescape/exception/RoomescapeException.java
 rename src/main/java/roomescape/{controller => exception}/RoomescapeExceptionHandler.java (52%)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index c3f7b867e7..85c23ba10c 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -1,9 +1,12 @@
 package roomescape.domain;
 
+import static roomescape.exception.ExceptionType.NAME_EMPTY;
+
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.Objects;
+import roomescape.exception.RoomescapeException;
 
 public class Reservation implements Comparable<Reservation> {
     private final Long id;
@@ -17,7 +20,7 @@ public Reservation(String name, LocalDate date, ReservationTime time) {
 
     public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
         if (name == null || name.isBlank()) {
-            throw new IllegalArgumentException("이름은 필수 값입니다.");
+            throw new RoomescapeException(NAME_EMPTY);
         }
         this.id = id;
         this.name = name;
diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index 63f0c19ef3..847a933d2c 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -1,7 +1,10 @@
 package roomescape.domain;
 
+import static roomescape.exception.ExceptionType.TIME_EMPTY;
+
 import java.time.LocalTime;
 import java.util.Objects;
+import roomescape.exception.RoomescapeException;
 
 public class ReservationTime {
     private final Long id;
@@ -14,7 +17,7 @@ public ReservationTime(LocalTime startAt) {
     //TODO 테스트 추가
     public ReservationTime(Long id, LocalTime startAt) {
         if (startAt == null) {
-            throw new IllegalArgumentException("시간은 없을 수 없습니다.");
+            throw new RoomescapeException(TIME_EMPTY);
         }
         this.id = id;
         this.startAt = startAt;
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
new file mode 100644
index 0000000000..ac9fb75a94
--- /dev/null
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -0,0 +1,35 @@
+package roomescape.exception;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+import org.springframework.http.HttpStatus;
+
+public enum ExceptionType {
+
+    NAME_EMPTY(BAD_REQUEST, "이름은 필수 값입니다."),
+    TIME_EMPTY(BAD_REQUEST, "시작 시간은 필수 값입니다."),
+    DUPLICATE_RESERVATION(BAD_REQUEST, "같은 시간에 이미 예약이 존재합니다."),
+    DUPLICATE_RESERVATION_TIME(BAD_REQUEST, "이미 예약시간이 존재합니다."),
+    INVALID_DATE_TIME_FORMAT(BAD_REQUEST, "해석할 수 없는 날짜, 시간 포맷입니다."),
+    //Todo 이름 변경
+    INVALID_DELETE_TIME(BAD_REQUEST, "예약이 존재하는 시간은 삭제할 수 없습니다."),
+    RESERVATION_TIME_NOT_FOUND(BAD_REQUEST, "존재하지 않는 시간입니다."),
+    //todo 이름 변경
+    PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다.");
+
+    private final HttpStatus status;
+    private final String message;
+
+    ExceptionType(HttpStatus status, String message) {
+        this.status = status;
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public HttpStatus getStatus() {
+        return status;
+    }
+}
diff --git a/src/main/java/roomescape/exception/RoomescapeException.java b/src/main/java/roomescape/exception/RoomescapeException.java
new file mode 100644
index 0000000000..5a647bbafd
--- /dev/null
+++ b/src/main/java/roomescape/exception/RoomescapeException.java
@@ -0,0 +1,20 @@
+package roomescape.exception;
+
+import org.springframework.http.HttpStatus;
+
+public class RoomescapeException extends RuntimeException {
+    private final ExceptionType exceptionType;
+
+    public RoomescapeException(ExceptionType exceptionType) {
+        this.exceptionType = exceptionType;
+    }
+
+    @Override
+    public String getMessage() {
+        return exceptionType.getMessage();
+    }
+
+    public HttpStatus getHttpStatus() {
+        return exceptionType.getStatus();
+    }
+}
diff --git a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java b/src/main/java/roomescape/exception/RoomescapeExceptionHandler.java
similarity index 52%
rename from src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
rename to src/main/java/roomescape/exception/RoomescapeExceptionHandler.java
index e7cc8497d2..c2a49f1fc9 100644
--- a/src/main/java/roomescape/controller/RoomescapeExceptionHandler.java
+++ b/src/main/java/roomescape/exception/RoomescapeExceptionHandler.java
@@ -1,4 +1,6 @@
-package roomescape.controller;
+package roomescape.exception;
+
+import static roomescape.exception.ExceptionType.INVALID_DATE_TIME_FORMAT;
 
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -12,15 +14,15 @@ public class RoomescapeExceptionHandler {
     @ExceptionHandler(HttpMessageNotReadableException.class)
     public ResponseEntity<ErrorResponse> handle(HttpMessageNotReadableException e) {
         e.printStackTrace();
-        return ResponseEntity.badRequest()
-                .body(new ErrorResponse("입력값이 잘못되었습니다."));
+        return ResponseEntity.status(INVALID_DATE_TIME_FORMAT.getStatus())
+                .body(new ErrorResponse(INVALID_DATE_TIME_FORMAT.getMessage()));
     }
 
-    //TODO : 커스템 에러로 처리하도록 변경
-    @ExceptionHandler(RuntimeException.class)
-    public ResponseEntity<ErrorResponse> handle(RuntimeException e) {
+    @ExceptionHandler(RoomescapeException.class)
+    public ResponseEntity<ErrorResponse> handle(RoomescapeException e) {
         e.printStackTrace();
-        return ResponseEntity.badRequest().
-                body(new ErrorResponse(e.getMessage()));
+        return ResponseEntity
+                .status(e.getHttpStatus())
+                .body(new ErrorResponse(e.getMessage()));
     }
 }
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 9327997a0d..a054867776 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,5 +1,9 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION;
+import static roomescape.exception.ExceptionType.PAST_TIME;
+import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
+
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -10,6 +14,7 @@
 import roomescape.dto.ReservationRequest;
 import roomescape.dto.ReservationResponse;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.exception.RoomescapeException;
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ReservationTimeRepository;
 
@@ -28,7 +33,7 @@ public ReservationService(ReservationRepository reservationRepository,
     public ReservationResponse save(ReservationRequest reservationRequest) {
         //TODO 변수명
         ReservationTime reservationTime = reservationTimeRepository.findById(reservationRequest.timeId())
-                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 시간입니다."));
+                .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
 
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
@@ -40,12 +45,12 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
                 .stream()
                 .anyMatch(reservation -> mapToLocalDateTime(reservation).equals(mapToLocalDateTime(beforeSave)));
         if (isDuplicate) {
-            throw new IllegalArgumentException("동일한 날짜와 시간에 이미 예약이 존재합니다.");
+            throw new RoomescapeException(DUPLICATE_RESERVATION);
         }
 
         //todo 테스트
         if (isBefore(beforeSave)) {
-            throw new IllegalArgumentException("이미 지난 시간에 예약할 수 없습니다.");
+            throw new RoomescapeException(PAST_TIME);
         }
 
         Reservation saved = reservationRepository.save(beforeSave);
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 09d12908a3..0b1b97b5c8 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -1,11 +1,15 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME;
+import static roomescape.exception.ExceptionType.INVALID_DELETE_TIME;
+
 import java.util.List;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
 import roomescape.dto.ReservationTimeRequest;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.exception.RoomescapeException;
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ReservationTimeRepository;
 
@@ -22,7 +26,7 @@ public ReservationTimeService(ReservationRepository reservationRepository,
 
     public ReservationTimeResponse save(ReservationTimeRequest reservationTimeRequest) {
         if (reservationTimeRepository.existsByStartAt(reservationTimeRequest.startAt())) {
-            throw new IllegalArgumentException("중복된 시간은 생성할 수 없습니다.");
+            throw new RoomescapeException(DUPLICATE_RESERVATION_TIME);
         }
         ReservationTime reservationTime = new ReservationTime(reservationTimeRequest.startAt());
         ReservationTime saved = reservationTimeRepository.save(reservationTime);
@@ -47,7 +51,7 @@ public void delete(long id) {
         boolean invalidDelete = reservations.stream()
                 .anyMatch(reservation -> reservation.getReservationTime().getId() == id);
         if (invalidDelete) {
-            throw new IllegalStateException("예약이 존재하는 시간을 지울 수 없습니다.");
+            throw new RoomescapeException(INVALID_DELETE_TIME);
         }
         reservationTimeRepository.delete(id);
     }
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index 05bfff4ced..cc622871d0 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -1,10 +1,13 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME;
+
 import java.time.LocalTime;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import roomescape.dto.ReservationTimeRequest;
+import roomescape.exception.RoomescapeException;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
 
@@ -22,8 +25,7 @@ void saveFailCauseDuplicate() {
         reservationTimeService.save(reservationTimeRequest);
 
         Assertions.assertThatThrownBy(() -> reservationTimeService.save(reservationTimeRequest))
-                .isInstanceOf(IllegalArgumentException.class)
-                //TODO 커스텀 에러
-                .hasMessage("중복된 시간은 생성할 수 없습니다.");
+                .isInstanceOf(RoomescapeException.class)
+                .hasMessage(DUPLICATE_RESERVATION_TIME.getMessage());
     }
 }

From 3e37f25132e9ebca5d9c105f5acdee3e5a43ed7d Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 20:12:29 +0900
Subject: [PATCH 18/75] =?UTF-8?q?docs:=20api=20=EB=AA=85=EC=84=B8=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index f7c1ea8c75..314cd1e4dc 100644
--- a/README.md
+++ b/README.md
@@ -58,9 +58,10 @@
 
 ### Response
 
-> HTTP/1.1 200
+> HTTP/1.1 201
 >
 > Content-Type: application/json
+> Location: /reservations/{id}
 
 ```JSON
 {
@@ -82,7 +83,7 @@
 
 ### Response
 
-> HTTP/1.1 200
+> HTTP/1.1 204
 
 ## 시간 추가 API
 
@@ -99,8 +100,9 @@
 
 ### response
 
-> HTTP/1.1 200
+> HTTP/1.1 201
 > Content-Type: application/json
+> Location: /times/{id}
 
 ```JSON
 {
@@ -137,4 +139,4 @@
 
 ### response
 
-> HTTP/1.1 200
+> HTTP/1.1 204

From 9d83ffeaf5c5f637d7a790d41948b2fcec88cd6f Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 20:22:56 +0900
Subject: [PATCH 19/75] =?UTF-8?q?docs:=20api=20=EB=AA=85=EC=84=B8=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/README.md b/README.md
index 314cd1e4dc..7e8e69f083 100644
--- a/README.md
+++ b/README.md
@@ -140,3 +140,65 @@
 ### response
 
 > HTTP/1.1 204
+
+## 테마 조회 API
+
+### request
+
+> GET /themes HTTP/1.1
+
+### response
+
+> HTTP/1.1 200
+> Content-Type: application/json
+
+```json
+[
+  {
+    "id": 1,
+    "name": "레벨2 탈출",
+    "description": "우테코 레벨2를 탈출하는 내용입니다.",
+    "thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  }
+]
+```
+
+## 테마 추가 API
+
+### request
+
+> POST /themes HTTP/1.1
+> content-type: application/json
+
+```json
+{
+  "name": "레벨2 탈출",
+  "description": "우테코 레벨2를 탈출하는 내용입니다.",
+  "thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}
+```
+
+### response
+
+> HTTP/1.1 201
+> Location: /themes/1
+> Content-Type: application/json
+
+```json
+{
+  "id": 1,
+  "name": "레벨2 탈출",
+  "description": "우테코 레벨2를 탈출하는 내용입니다.",
+  "thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}
+```
+
+## 테마 삭제 API
+
+### request
+
+> DELETE /themes/1 HTTP/1.1
+
+### response
+
+> HTTP/1.1 204

From 6f1be1a8648d2aca4f2281f9fd355c4ad9b59d9c Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 20:26:51 +0900
Subject: [PATCH 20/75] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9?=
 =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/README.md b/README.md
index 7e8e69f083..2307ca89c8 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,13 @@
     - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [x] 지나간 날짜와 시간의 예약 요청 시 에러
     - [x] 이름이 비어있는 예약 요청 시 에러
+    - [ ] 존재하지 않는 테마 예약 생성 요청시 에러
+    - [ ] 테마 값이 비어있는 예약 요청 시 에러
+
+- [ ] 테마에 대한 제약 조건 추가
+    - [ ] 테마 이름이 비어 있을 경우 에러
+    - [ ] 중복된 이름의 테마 생성 요청시 에러
+    - [ ] 예약이 있는 테마를 삭제 요청시 에러
 
 # API 명세
 

From c75c4852542df80fb71e5ac484e60d49a666297f Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 20:27:53 +0900
Subject: [PATCH 21/75] =?UTF-8?q?feat:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?=
 =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EB=B0=98=ED=99=98?=
 =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 src/main/java/roomescape/controller/AdminController.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/roomescape/controller/AdminController.java b/src/main/java/roomescape/controller/AdminController.java
index cbdee6650a..cf9bfaee2e 100644
--- a/src/main/java/roomescape/controller/AdminController.java
+++ b/src/main/java/roomescape/controller/AdminController.java
@@ -14,7 +14,7 @@ public String mainPage() {
 
     @GetMapping("/reservation")
     public String reservationPage() {
-        return "admin/reservation";
+        return "admin/reservation-new";
     }
 
     @GetMapping("/time")

From 4d4c98526302dd3ca3b3666d9f355c77e291c3cc Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 21:16:38 +0900
Subject: [PATCH 22/75] =?UTF-8?q?feat:=20=ED=85=8C=EB=A7=88=20CRD=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 테마 이름, 설명, 썸네일 이미자가 비어 있을 경우 에러
- 중복된 이름의 테마 생성 요청시 에러

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                     |  4 +-
 .../controller/AdminController.java           |  5 ++
 .../controller/ThemeController.java           | 44 +++++++++++++
 src/main/java/roomescape/domain/Theme.java    | 64 +++++++++++++++++++
 .../java/roomescape/dto/ThemeRequest.java     |  4 ++
 .../java/roomescape/dto/ThemeResponse.java    |  4 ++
 .../roomescape/exception/ExceptionType.java   |  7 +-
 .../JdbcTemplateThemeRepository.java          | 54 ++++++++++++++++
 .../repository/ThemeRepository.java           | 12 ++++
 .../java/roomescape/service/ThemeService.java | 46 +++++++++++++
 src/main/resources/schema.sql                 | 15 ++++-
 src/test/resources/schema.sql                 | 15 ++++-
 12 files changed, 266 insertions(+), 8 deletions(-)
 create mode 100644 src/main/java/roomescape/controller/ThemeController.java
 create mode 100644 src/main/java/roomescape/domain/Theme.java
 create mode 100644 src/main/java/roomescape/dto/ThemeRequest.java
 create mode 100644 src/main/java/roomescape/dto/ThemeResponse.java
 create mode 100644 src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
 create mode 100644 src/main/java/roomescape/repository/ThemeRepository.java
 create mode 100644 src/main/java/roomescape/service/ThemeService.java

diff --git a/README.md b/README.md
index 2307ca89c8..5ac7fb2187 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,8 @@
     - [ ] 테마 값이 비어있는 예약 요청 시 에러
 
 - [ ] 테마에 대한 제약 조건 추가
-    - [ ] 테마 이름이 비어 있을 경우 에러
-    - [ ] 중복된 이름의 테마 생성 요청시 에러
+    - [x] 테마 이름, 설명, 썸네일 이미자가 비어 있을 경우 에러
+    - [x] 중복된 이름의 테마 생성 요청시 에러
     - [ ] 예약이 있는 테마를 삭제 요청시 에러
 
 # API 명세
diff --git a/src/main/java/roomescape/controller/AdminController.java b/src/main/java/roomescape/controller/AdminController.java
index cf9bfaee2e..4e9a8510fa 100644
--- a/src/main/java/roomescape/controller/AdminController.java
+++ b/src/main/java/roomescape/controller/AdminController.java
@@ -21,4 +21,9 @@ public String reservationPage() {
     public String reservationTimePage() {
         return "admin/time";
     }
+
+    @GetMapping("/theme")
+    public String themePage() {
+        return "admin/theme";
+    }
 }
diff --git a/src/main/java/roomescape/controller/ThemeController.java b/src/main/java/roomescape/controller/ThemeController.java
new file mode 100644
index 0000000000..bc38a24576
--- /dev/null
+++ b/src/main/java/roomescape/controller/ThemeController.java
@@ -0,0 +1,44 @@
+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 roomescape.dto.ThemeRequest;
+import roomescape.dto.ThemeResponse;
+import roomescape.service.ThemeService;
+
+//Todo 테스트코드 작성
+@RestController
+@RequestMapping("/themes")
+public class ThemeController {
+    private final ThemeService themeService;
+
+    public ThemeController(ThemeService themeService) {
+        this.themeService = themeService;
+    }
+
+    @GetMapping
+    public List<ThemeResponse> findAll() {
+        return themeService.findAll();
+    }
+
+    @PostMapping
+    public ResponseEntity<ThemeResponse> save(@RequestBody ThemeRequest themeRequest) {
+        ThemeResponse saved = themeService.save(themeRequest);
+        return ResponseEntity.created(URI.create("/themes/" + saved.id()))
+                .body(saved);
+    }
+
+    @DeleteMapping("/{id}")
+    public ResponseEntity<Void> delete(@PathVariable long id) {
+        themeService.delete(id);
+        return ResponseEntity.noContent().build();
+    }
+}
diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
new file mode 100644
index 0000000000..407b60c7f6
--- /dev/null
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -0,0 +1,64 @@
+package roomescape.domain;
+
+import roomescape.exception.ExceptionType;
+import roomescape.exception.RoomescapeException;
+
+public class Theme {
+    private final Long id;
+    private final String name;
+    private final String description;
+    private final String thumbnail;
+
+    public Theme(long id, Theme theme) {
+        this(id, theme.name, theme.description, theme.thumbnail);
+    }
+
+    public Theme(Long id, String name, String description, String thumbnail) {
+        validateName(name);
+        validateDescription(description);
+        validateThumbnail(thumbnail);
+        this.id = id;
+        this.name = name;
+        this.description = description;
+        this.thumbnail = thumbnail;
+    }
+
+    private void validateName(String name) {
+        if (name == null || name.isBlank()) {
+            //TODO : NAME_EMPYT 재사용 고민
+            throw new RoomescapeException(ExceptionType.NAME_EMPTY);
+        }
+    }
+
+    private void validateDescription(String description) {
+        if (description == null || description.isBlank()) {
+            throw new RoomescapeException(ExceptionType.DESCRIPTION_EMPTY);
+        }
+    }
+
+    private void validateThumbnail(String thumbnail) {
+        if (thumbnail == null || thumbnail.isBlank()) {
+            throw new RoomescapeException(ExceptionType.THUMBNAIL_EMPTY);
+        }
+    }
+
+    public Theme(String name, String description, String thumbnail) {
+        this(null, name, description, thumbnail);
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getThumbnail() {
+        return thumbnail;
+    }
+}
diff --git a/src/main/java/roomescape/dto/ThemeRequest.java b/src/main/java/roomescape/dto/ThemeRequest.java
new file mode 100644
index 0000000000..bbc725828e
--- /dev/null
+++ b/src/main/java/roomescape/dto/ThemeRequest.java
@@ -0,0 +1,4 @@
+package roomescape.dto;
+
+public record ThemeRequest(String name, String description, String thumbnail) {
+}
diff --git a/src/main/java/roomescape/dto/ThemeResponse.java b/src/main/java/roomescape/dto/ThemeResponse.java
new file mode 100644
index 0000000000..6cd1eab9be
--- /dev/null
+++ b/src/main/java/roomescape/dto/ThemeResponse.java
@@ -0,0 +1,4 @@
+package roomescape.dto;
+
+public record ThemeResponse(long id, String name, String description, String thumbnail) {
+}
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index ac9fb75a94..f6fd3c2e1b 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -10,12 +10,15 @@ public enum ExceptionType {
     TIME_EMPTY(BAD_REQUEST, "시작 시간은 필수 값입니다."),
     DUPLICATE_RESERVATION(BAD_REQUEST, "같은 시간에 이미 예약이 존재합니다."),
     DUPLICATE_RESERVATION_TIME(BAD_REQUEST, "이미 예약시간이 존재합니다."),
+    DUPLICATE_THEME(BAD_REQUEST, "이미 동일한 테마가 존재합니다."),
     INVALID_DATE_TIME_FORMAT(BAD_REQUEST, "해석할 수 없는 날짜, 시간 포맷입니다."),
     //Todo 이름 변경
     INVALID_DELETE_TIME(BAD_REQUEST, "예약이 존재하는 시간은 삭제할 수 없습니다."),
     RESERVATION_TIME_NOT_FOUND(BAD_REQUEST, "존재하지 않는 시간입니다."),
-    //todo 이름 변경
-    PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다.");
+    //todo 이름 변경,
+    PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
+    DESCRIPTION_EMPTY(BAD_REQUEST, "테마 설명은 필수값 입니다."),
+    THUMBNAIL_EMPTY(BAD_REQUEST, "테마 썸네일은 필수값 입니다.");
 
     private final HttpStatus status;
     private final String message;
diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
new file mode 100644
index 0000000000..d1521f76a0
--- /dev/null
+++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
@@ -0,0 +1,54 @@
+package roomescape.repository;
+
+import java.sql.PreparedStatement;
+import java.util.List;
+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.domain.Theme;
+
+@Repository
+public class JdbcTemplateThemeRepository implements ThemeRepository {
+    private final JdbcTemplate jdbcTemplate;
+
+    public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    @Override
+    public List<Theme> findAll() {
+        return jdbcTemplate.query("select ID, NAME, DESCRIPTION, THUMBNAIL from THEME", (rs, rowNum) -> {
+            long id = rs.getLong(1);
+            String name = rs.getString(2);
+            String description = rs.getString(3);
+            String thumbnail = rs.getString(4);
+            return new Theme(id, name, description, thumbnail);
+        });
+    }
+
+    @Override
+    public Theme save(Theme theme) {
+        KeyHolder keyHolder = new GeneratedKeyHolder();
+        save(theme, keyHolder);
+        long id = keyHolder.getKey().longValue();
+        return new Theme(id, theme);
+    }
+
+    private void save(Theme theme, KeyHolder keyHolder) {
+        jdbcTemplate.update(con -> {
+                    PreparedStatement pstm = con.prepareStatement(
+                            "insert into THEME (NAME, DESCRIPTION, THUMBNAIL) values (?, ?, ?) ", new String[]{"id"});
+                    pstm.setString(1, theme.getName());
+                    pstm.setString(2, theme.getDescription());
+                    pstm.setString(3, theme.getThumbnail());
+                    return pstm;
+                },
+                keyHolder);
+    }
+
+    @Override
+    public void delete(long id) {
+        jdbcTemplate.update("delete from THEME where id = ?", id);
+    }
+}
diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java
new file mode 100644
index 0000000000..c1fd6c88a7
--- /dev/null
+++ b/src/main/java/roomescape/repository/ThemeRepository.java
@@ -0,0 +1,12 @@
+package roomescape.repository;
+
+import java.util.List;
+import roomescape.domain.Theme;
+
+public interface ThemeRepository {
+    List<Theme> findAll();
+
+    Theme save(Theme theme);
+
+    void delete(long id);
+}
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
new file mode 100644
index 0000000000..f62507dfd3
--- /dev/null
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -0,0 +1,46 @@
+package roomescape.service;
+
+import java.util.List;
+import org.springframework.stereotype.Service;
+import roomescape.domain.Theme;
+import roomescape.dto.ThemeRequest;
+import roomescape.dto.ThemeResponse;
+import roomescape.exception.ExceptionType;
+import roomescape.exception.RoomescapeException;
+import roomescape.repository.ThemeRepository;
+
+//todo 테스트코드 작성
+@Service
+public class ThemeService {
+
+    private final ThemeRepository themeRepository;
+
+    public ThemeService(ThemeRepository themeRepository) {
+        this.themeRepository = themeRepository;
+    }
+
+    public ThemeResponse save(ThemeRequest themeRequest) {
+        boolean hasDuplicateTheme = themeRepository.findAll().stream()
+                .anyMatch(theme -> theme.getName().equals(themeRequest.name()));
+        if (hasDuplicateTheme) {
+            throw new RoomescapeException(ExceptionType.DUPLICATE_THEME);
+        }
+        Theme saved = themeRepository.save(
+                new Theme(themeRequest.name(), themeRequest.description(), themeRequest.thumbnail()));
+        return toResponse(saved);
+    }
+
+    private ThemeResponse toResponse(Theme theme) {
+        return new ThemeResponse(theme.getId(), theme.getName(), theme.getDescription(), theme.getThumbnail());
+    }
+
+    public List<ThemeResponse> findAll() {
+        return themeRepository.findAll().stream()
+                .map(this::toResponse)
+                .toList();
+    }
+
+    public void delete(long id) {
+        themeRepository.delete(id);
+    }
+}
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 44a68814dd..d246ddc4a6 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -5,12 +5,23 @@ CREATE TABLE IF NOT EXISTS reservation_time
     PRIMARY KEY (id)
 );
 
+CREATE TABLE IF NOT EXISTS theme
+(
+    id          BIGINT       NOT NULL AUTO_INCREMENT,
+    name        VARCHAR(255) NOT NULL,
+    description VARCHAR(255) NOT NULL,
+    thumbnail VARCHAR(255) NOT NULL,
+    PRIMARY KEY (id)
+);
+
 CREATE TABLE IF NOT EXISTS reservation
 (
     id   BIGINT       NOT NULL AUTO_INCREMENT,
     name VARCHAR(255) NOT NULL,
     date VARCHAR(255) NOT NULL,
-    time_id BIGINT NOT NULL,                           -- 컬럼 수정
+    time_id BIGINT    NOT NULL,
+    -- theme_id BIGINT   NOT NULL,
     PRIMARY KEY (id),
-    FOREIGN KEY (time_id) REFERENCES reservation_time (id) -- 외래키 추가
+    FOREIGN KEY (time_id) REFERENCES reservation_time (id)
+    -- FOREIGN KEY (theme_id) REFERENCES theme (id)
 );
diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql
index 0945a159b6..991d50c4a7 100644
--- a/src/test/resources/schema.sql
+++ b/src/test/resources/schema.sql
@@ -5,12 +5,23 @@ CREATE TABLE IF NOT EXISTS reservation_time
     PRIMARY KEY (id)
 );
 
+CREATE TABLE IF NOT EXISTS theme
+(
+    id          BIGINT       NOT NULL AUTO_INCREMENT,
+    name        VARCHAR(255) NOT NULL,
+    description VARCHAR(255) NOT NULL,
+    thumbnail VARCHAR(255) NOT NULL,
+    PRIMARY KEY (id)
+);
+
 CREATE TABLE IF NOT EXISTS reservation
 (
     id   BIGINT       NOT NULL AUTO_INCREMENT,
     name VARCHAR(255) NOT NULL,
     date VARCHAR(255) NOT NULL,
-    time_id BIGINT,                           -- 컬럼 수정
+    time_id BIGINT    NOT NULL,
+    theme_id BIGINT   NOT NULL,
     PRIMARY KEY (id),
-    FOREIGN KEY (time_id) REFERENCES reservation_time (id) -- 외래키 추가
+    FOREIGN KEY (time_id) REFERENCES reservation_time (id),
+    FOREIGN KEY (theme_id) REFERENCES theme (id)
 );

From 44c19743cf1cc8f7d3bb5027d6a7f2f28dfa58b2 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 30 Apr 2024 22:26:07 +0900
Subject: [PATCH 23/75] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=EC=97=90?=
 =?UTF-8?q?=EC=95=BD=20API=20=EC=97=90=20=ED=85=8C=EB=A7=88=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                     | 15 ++++++-
 .../java/roomescape/domain/Reservation.java   | 15 +++++--
 .../roomescape/dto/ReservationRequest.java    |  2 +-
 .../roomescape/dto/ReservationResponse.java   |  3 +-
 .../roomescape/exception/ExceptionType.java   |  4 +-
 .../JdbcTemplateReservationRepository.java    | 39 +++++++++++++------
 .../JdbcTemplateThemeRepository.java          | 13 +++++++
 .../repository/ThemeRepository.java           |  3 ++
 .../service/ReservationService.java           | 19 +++++++--
 src/main/resources/schema.sql                 |  6 +--
 .../resources/static/js/reservation-new.js    |  1 +
 .../controller/AdminControllerTest.java       |  2 +-
 .../controller/ReservationControllerTest.java | 24 ++++++++----
 .../roomescape/domain/ReservationTest.java    |  7 +++-
 .../integration/AdminIntegrationTest.java     |  5 +++
 .../CollectionReservationRepository.java      |  3 +-
 .../repository/CollectionThemeRepository.java | 27 +++++++++++++
 ...JdbcTemplateReservationRepositoryTest.java | 12 ++++--
 18 files changed, 159 insertions(+), 41 deletions(-)
 create mode 100644 src/test/java/roomescape/repository/CollectionThemeRepository.java

diff --git a/README.md b/README.md
index 5ac7fb2187..1d935687fd 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,12 @@
             "id": 1,
             "startAt": "10:00"
         }
+        "theme" : {
+            "id": 1,
+            "name": "이름",
+            "description": "설명",
+            "thumbnail": "썸네일"
+        }
     }
 ]
 ```
@@ -59,7 +65,8 @@
 {
   "date": "2023-08-05",
   "name": "브라운",
-  "timeId": 1
+  "timeId": 1,
+  "themeId": 1
 }
 ```
 
@@ -78,6 +85,12 @@
   "time": {
     "id": 1,
     "startAt": "10:00"
+  },
+  "theme": {
+    "id": 1,
+    "name": "이름",
+    "description": "설명",
+    "thumbnail": "썸네일"
   }
 }
 ```
diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index 85c23ba10c..a5d2c3e45d 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -13,12 +13,13 @@ public class Reservation implements Comparable<Reservation> {
     private final String name;
     private final LocalDate date;
     private final ReservationTime time;
+    private final Theme theme;
 
-    public Reservation(String name, LocalDate date, ReservationTime time) {
-        this(null, name, date, time);
+    public Reservation(String name, LocalDate date, ReservationTime time, Theme theme) {
+        this(null, name, date, time, theme);
     }
 
-    public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
+    public Reservation(Long id, String name, LocalDate date, ReservationTime time, Theme theme) {
         if (name == null || name.isBlank()) {
             throw new RoomescapeException(NAME_EMPTY);
         }
@@ -26,10 +27,12 @@ public Reservation(Long id, String name, LocalDate date, ReservationTime time) {
         this.name = name;
         this.date = date;
         this.time = time;
+        this.theme = theme;
     }
 
     public Reservation(long id, Reservation reservationBeforeSave) {
-        this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time);
+        this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time,
+                reservationBeforeSave.theme);
     }
 
     @Override
@@ -63,6 +66,10 @@ public ReservationTime getReservationTime() {
         return time;
     }
 
+    public Theme getTheme() {
+        return theme;
+    }
+
     @Override
     public int hashCode() {
         int result = id != null ? id.hashCode() : 0;
diff --git a/src/main/java/roomescape/dto/ReservationRequest.java b/src/main/java/roomescape/dto/ReservationRequest.java
index ad5cb41a22..fe3e4ec125 100644
--- a/src/main/java/roomescape/dto/ReservationRequest.java
+++ b/src/main/java/roomescape/dto/ReservationRequest.java
@@ -2,5 +2,5 @@
 
 import java.time.LocalDate;
 
-public record ReservationRequest(LocalDate date, String name, long timeId) {
+public record ReservationRequest(LocalDate date, String name, long timeId, long themeId) {
 }
diff --git a/src/main/java/roomescape/dto/ReservationResponse.java b/src/main/java/roomescape/dto/ReservationResponse.java
index d9980c55fd..20bd66cb9a 100644
--- a/src/main/java/roomescape/dto/ReservationResponse.java
+++ b/src/main/java/roomescape/dto/ReservationResponse.java
@@ -2,5 +2,6 @@
 
 import java.time.LocalDate;
 
-public record ReservationResponse(long id, String name, LocalDate date, ReservationTimeResponse time) {
+public record ReservationResponse(long id, String name, LocalDate date, ReservationTimeResponse time,
+                                  ThemeResponse theme) {
 }
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index f6fd3c2e1b..038a88ce56 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -18,7 +18,9 @@ public enum ExceptionType {
     //todo 이름 변경,
     PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
     DESCRIPTION_EMPTY(BAD_REQUEST, "테마 설명은 필수값 입니다."),
-    THUMBNAIL_EMPTY(BAD_REQUEST, "테마 썸네일은 필수값 입니다.");
+    THUMBNAIL_EMPTY(BAD_REQUEST, "테마 썸네일은 필수값 입니다."),
+    THEME_NOT_FOUND(BAD_REQUEST, "없는 테마입니다."),
+    ;
 
     private final HttpStatus status;
     private final String message;
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index 31eef64900..213d7ef6f4 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -11,6 +11,7 @@
 import org.springframework.stereotype.Repository;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 @Repository
 public class JdbcTemplateReservationRepository implements ReservationRepository {
@@ -24,7 +25,7 @@ public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) {
     public Reservation save(Reservation reservation) {
         ReservationTime reservationTime = findReservationTime(reservation.getReservationTime().getId());
         Reservation beforeSaved = new Reservation(null, reservation.getName(), reservation.getDate(),
-                reservationTime);
+                reservationTime, reservation.getTheme());
         KeyHolder keyHolder = new GeneratedKeyHolder();
         save(beforeSaved, keyHolder);
         long id = keyHolder.getKey().longValue();
@@ -42,26 +43,35 @@ private ReservationTime findReservationTime(long timeId) {
 
     private void save(Reservation reservation, KeyHolder keyHolder) {
         jdbcTemplate.update(con -> {
-            String sql = "insert into reservation(name,date,time_id) values ( ?,?,? )";
+            String sql = "insert into reservation(name,date,time_id,THEME_ID) values ( ?,?,?,? )";
             PreparedStatement preparedStatement = con.prepareStatement(sql, new String[]{"id"});
             preparedStatement.setString(1, reservation.getName());
             preparedStatement.setDate(2, Date.valueOf(reservation.getDate()));
             preparedStatement.setLong(3, reservation.getReservationTime().getId());
+            preparedStatement.setLong(4, reservation.getTheme().getId());
             return preparedStatement;
         }, keyHolder);
     }
 
+    //Todo 개선 고민
     @Override
     public List<Reservation> findAll() {
-        String query = "SELECT "
-                + "    r.id as reservation_id,"
-                + "    r.name,"
-                + "    r.date,"
-                + "    t.id as time_id,"
-                + "    t.start_at as time_value"
-                + " FROM reservation as r"
-                + " inner join reservation_time as t"
-                + " on r.time_id = t.id";
+        String query = """
+                   SELECT 
+                   r.id as reservation_id,
+                   r.name,
+                   r.date,
+                   t.id as time_id,
+                   t.start_at as time_value,
+                   t2.id as theme_id,
+                   t2.NAME as theme_name,
+                   t2.DESCRIPTION as description,
+                   t2.THUMBNAIL as thumbnail
+                FROM reservation as r
+                inner join reservation_time t 
+                on r.time_id = t.id
+                inner join theme t2  
+                on t2.id = r.theme_id""";
         return jdbcTemplate.query(query,
                 (rs, rowNum) -> {
                     long id = rs.getLong(1);
@@ -70,7 +80,12 @@ public List<Reservation> findAll() {
                     long timeId = rs.getLong(4);
                     LocalTime startAt = rs.getTime(5).toLocalTime();
                     ReservationTime reservationTime = new ReservationTime(timeId, startAt);
-                    return new Reservation(id, name, date, reservationTime);
+                    long themeId = rs.getLong("theme_id");
+                    String themeName = rs.getString("theme_name");
+                    String description = rs.getString("description");
+                    String thumbnail = rs.getString("thumbnail");
+                    Theme theme = new Theme(themeId, themeName, description, thumbnail);
+                    return new Reservation(id, name, date, reservationTime, theme);
                 });
     }
 
diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
index d1521f76a0..7d44bd86be 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
@@ -2,6 +2,7 @@
 
 import java.sql.PreparedStatement;
 import java.util.List;
+import java.util.Optional;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.support.GeneratedKeyHolder;
 import org.springframework.jdbc.support.KeyHolder;
@@ -27,6 +28,18 @@ public List<Theme> findAll() {
         });
     }
 
+    @Override
+    public Optional<Theme> findById(long id) {
+        List<Theme> themes = jdbcTemplate.query("select id, name, description, thumbnail from theme where id = ?",
+                (rs, rowNum) -> {
+                    String name = rs.getString("name");
+                    String description = rs.getString("description");
+                    String thumbnail = rs.getString("thumbnail");
+                    return new Theme(id, name, description, thumbnail);
+                }, id);
+        return themes.stream().findFirst();
+    }
+
     @Override
     public Theme save(Theme theme) {
         KeyHolder keyHolder = new GeneratedKeyHolder();
diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java
index c1fd6c88a7..a43cb24851 100644
--- a/src/main/java/roomescape/repository/ThemeRepository.java
+++ b/src/main/java/roomescape/repository/ThemeRepository.java
@@ -1,11 +1,14 @@
 package roomescape.repository;
 
 import java.util.List;
+import java.util.Optional;
 import roomescape.domain.Theme;
 
 public interface ThemeRepository {
     List<Theme> findAll();
 
+    Optional<Theme> findById(long id);
+
     Theme save(Theme theme);
 
     void delete(long id);
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index a054867776..2980fdae95 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -11,22 +11,28 @@
 import org.springframework.stereotype.Service;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 import roomescape.dto.ReservationRequest;
 import roomescape.dto.ReservationResponse;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.dto.ThemeResponse;
+import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ReservationTimeRepository;
+import roomescape.repository.ThemeRepository;
 
 @Service
 public class ReservationService {
     private final ReservationRepository reservationRepository;
     private final ReservationTimeRepository reservationTimeRepository;
+    private final ThemeRepository themeRepository;
 
     public ReservationService(ReservationRepository reservationRepository,
-                              ReservationTimeRepository reservationTimeRepository) {
+                              ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository) {
         this.reservationRepository = reservationRepository;
         this.reservationTimeRepository = reservationTimeRepository;
+        this.themeRepository = themeRepository;
     }
 
     //ToDo 테스트 작성
@@ -35,11 +41,15 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         ReservationTime reservationTime = reservationTimeRepository.findById(reservationRequest.timeId())
                 .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
 
+        Theme theme = themeRepository.findById(reservationRequest.themeId())
+                .orElseThrow(() -> new RoomescapeException(ExceptionType.THEME_NOT_FOUND));
+
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
                 reservationRequest.date(),
                 //TODO : 커스텀 예외 사용할지 고민해보기
-                reservationTime
+                reservationTime,
+                theme
         );
         boolean isDuplicate = reservationRepository.findAll()
                 .stream()
@@ -72,8 +82,11 @@ private ReservationResponse toResponse(Reservation reservation) {
         ReservationTime reservationTime = reservation.getReservationTime();
         ReservationTimeResponse reservationTimeResponse = new ReservationTimeResponse(reservationTime.getId(),
                 reservation.getTime());
+        Theme theme = reservation.getTheme();
+        ThemeResponse themeResponse = new ThemeResponse(theme.getId(), theme.getName(), theme.getDescription(),
+                theme.getThumbnail());
         return new ReservationResponse(reservation.getId(),
-                reservation.getName(), reservation.getDate(), reservationTimeResponse);
+                reservation.getName(), reservation.getDate(), reservationTimeResponse, themeResponse);
     }
 
     public List<ReservationResponse> findAll() {
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index d246ddc4a6..991d50c4a7 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -20,8 +20,8 @@ CREATE TABLE IF NOT EXISTS reservation
     name VARCHAR(255) NOT NULL,
     date VARCHAR(255) NOT NULL,
     time_id BIGINT    NOT NULL,
-    -- theme_id BIGINT   NOT NULL,
+    theme_id BIGINT   NOT NULL,
     PRIMARY KEY (id),
-    FOREIGN KEY (time_id) REFERENCES reservation_time (id)
-    -- FOREIGN KEY (theme_id) REFERENCES theme (id)
+    FOREIGN KEY (time_id) REFERENCES reservation_time (id),
+    FOREIGN KEY (theme_id) REFERENCES theme (id)
 );
diff --git a/src/main/resources/static/js/reservation-new.js b/src/main/resources/static/js/reservation-new.js
index 098b8b70d8..2d8e132cce 100644
--- a/src/main/resources/static/js/reservation-new.js
+++ b/src/main/resources/static/js/reservation-new.js
@@ -27,6 +27,7 @@ function render(data) {
     TODO: [2단계] 관리자 기능 - 예약 목록 조회 API 호출 후 렌더링
           response 명세에 맞춰 값 설정
     */
+    console.log(item);
     row.insertCell(0).textContent = item.id;            // 예약 id
     row.insertCell(1).textContent = item.name;          // 예약자명
     row.insertCell(2).textContent = item.theme.name;    // 테마명
diff --git a/src/test/java/roomescape/controller/AdminControllerTest.java b/src/test/java/roomescape/controller/AdminControllerTest.java
index 832a31fc0e..adeb4f8b2c 100644
--- a/src/test/java/roomescape/controller/AdminControllerTest.java
+++ b/src/test/java/roomescape/controller/AdminControllerTest.java
@@ -20,7 +20,7 @@ void reservationPage() {
         AdminController adminController = new AdminController();
         String reservationPage = adminController.reservationPage();
         Assertions.assertThat(reservationPage)
-                .isEqualTo("admin/reservation");
+                .isEqualTo("admin/reservation-new");
     }
 
     @Test
diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index fa257e5e74..cbba143c3e 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -11,36 +11,43 @@
 import org.junit.jupiter.api.Test;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 import roomescape.dto.ReservationRequest;
 import roomescape.dto.ReservationResponse;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.dto.ThemeResponse;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.CollectionThemeRepository;
 import roomescape.service.ReservationService;
 
+//TODO : 전체적으로 테스트 수정
 class ReservationControllerTest {
     static final long timeId = 1L;
     static final LocalTime time = LocalTime.now();
+    private static final Theme DEFUALT_THEME = new Theme(1L, "이름", "설명", "썸네일");
     private final CollectionReservationTimeRepository timeRepository = new CollectionReservationTimeRepository(
             new ArrayList<>(List.of(new ReservationTime(timeId, time)))
     );
+    private final CollectionThemeRepository themeRepository = new CollectionThemeRepository();
 
     @Test
     @DisplayName("예약 정보를 잘 저장하는지 확인한다.")
     void saveReservation() {
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
+                themeRepository);
         ReservationController reservationController = new ReservationController(reservationService);
         LocalDate date = LocalDate.now().plusDays(1);
 
         ReservationResponse saveResponse = reservationController.saveReservation(
-                        new ReservationRequest(date, "폴라", timeId))
+                        new ReservationRequest(date, "폴라", timeId, 1))
                 .getBody();
 
         long id = Objects.requireNonNull(saveResponse).id();
         ReservationResponse expected = new ReservationResponse(id, "폴라", date,
-                new ReservationTimeResponse(timeId, time));
+                new ReservationTimeResponse(timeId, time), new ThemeResponse(1, "이름", "설명", "썸네일"));
 
         Assertions.assertThat(saveResponse)
                 .isEqualTo(expected);
@@ -51,7 +58,8 @@ void saveReservation() {
     void findAllReservations() {
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
+                null);
         ReservationController reservationController = new ReservationController(reservationService);
         List<ReservationResponse> allReservations = reservationController.findAllReservations();
 
@@ -63,10 +71,11 @@ void findAllReservations() {
     @DisplayName("예약 정보를 잘 지우는지 확인한다.")
     void delete() {
         List<Reservation> reservations = List.of(
-                new Reservation(1L, "폴라", LocalDate.now(), new ReservationTime(LocalTime.now())));
+                new Reservation(1L, "폴라", LocalDate.now(), new ReservationTime(LocalTime.now()), DEFUALT_THEME));
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 new ArrayList<>(reservations), timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
+                null);
         ReservationController reservationController = new ReservationController(reservationService);
 
         reservationController.delete(1L);
@@ -81,7 +90,8 @@ void delete() {
     void checkRepositoryDependency() {
         CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
                 timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
+                null);
         ReservationController reservationController = new ReservationController(reservationService);
 
         boolean isRepositoryInjected = false;
diff --git a/src/test/java/roomescape/domain/ReservationTest.java b/src/test/java/roomescape/domain/ReservationTest.java
index 63bc689091..571020049a 100644
--- a/src/test/java/roomescape/domain/ReservationTest.java
+++ b/src/test/java/roomescape/domain/ReservationTest.java
@@ -7,13 +7,16 @@
 import org.junit.jupiter.api.Test;
 
 class ReservationTest {
+
+    private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "썸네일");
+
     @Test
     @DisplayName("날짜를 기준으로 비교를 잘 하는지 확인.")
     void compareTo() {
         Reservation first = new Reservation(1L, "폴라", LocalDate.of(1999, 12, 1), new ReservationTime(
-                LocalTime.of(16, 30)));
+                LocalTime.of(16, 30)), DEFAULT_THEME);
         Reservation second = new Reservation(2L, "로빈", LocalDate.of(1998, 1, 8), new ReservationTime(
-                LocalTime.of(16, 30)));
+                LocalTime.of(16, 30)), DEFAULT_THEME);
         int compareTo = first.compareTo(second);
         Assertions.assertThat(compareTo)
                 .isGreaterThan(0);
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
index 9a525e8289..2db65f8542 100644
--- a/src/test/java/roomescape/integration/AdminIntegrationTest.java
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -33,6 +33,9 @@ void init() {
         jdbcTemplate.update("delete from reservation_time");
         jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
         jdbcTemplate.update("insert into reservation_time(start_at) values('11:56')");
+        jdbcTemplate.update("delete from THEME");
+        jdbcTemplate.update("ALTER TABLE THEME alter column id restart with 1");
+        jdbcTemplate.update("insert into THEME values ( 1,'a','a','a')");
         RestAssured.port = port;
     }
 
@@ -70,6 +73,7 @@ void adminReservationPageWork() {
         params.put("name", "브라운");
         params.put("date", LocalDate.now().plusDays(1).format(DATE_FORMATTER));
         params.put("timeId", 1);
+        params.put("themeId", 1);
 
         RestAssured.given().log().all()
                 .contentType(ContentType.JSON)
@@ -104,6 +108,7 @@ void adminReservationPageWorkWithDB() {
         params.put("name", "브라운");
         params.put("date", LocalDate.now().plusDays(1).format(DATE_FORMATTER));
         params.put("timeId", 1);
+        params.put("themeId", 1);
 
         RestAssured.given().log().all()
                 .contentType(ContentType.JSON)
diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java
index ef0eac28b5..213022259a 100644
--- a/src/test/java/roomescape/repository/CollectionReservationRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java
@@ -35,12 +35,13 @@ private static Predicate<ReservationTime> sameId(ReservationRequest reservationR
 
     @Override
     public Reservation save(Reservation reservation) {
+        //TODO 안해도 되는지 확인하기
         ReservationTime findTime = timeRepository.findAll().stream()
                 .filter(reservationTime -> reservationTime.getId() == reservation.getReservationTime().getId())
                 .findFirst()
                 .get();
         Reservation saved = new Reservation(atomicLong.incrementAndGet(), reservation.getName(), reservation.getDate(),
-                findTime);
+                findTime, reservation.getTheme());
         reservations.add(saved);
         return saved;
     }
diff --git a/src/test/java/roomescape/repository/CollectionThemeRepository.java b/src/test/java/roomescape/repository/CollectionThemeRepository.java
new file mode 100644
index 0000000000..a851648ec3
--- /dev/null
+++ b/src/test/java/roomescape/repository/CollectionThemeRepository.java
@@ -0,0 +1,27 @@
+package roomescape.repository;
+
+import java.util.List;
+import java.util.Optional;
+import roomescape.domain.Theme;
+
+public class CollectionThemeRepository implements ThemeRepository {
+    @Override
+    public List<Theme> findAll() {
+        return null;
+    }
+
+    @Override
+    public Optional<Theme> findById(long id) {
+        return Optional.of(new Theme(id, "이름", "설명", "썸네일"));
+    }
+
+    @Override
+    public Theme save(Theme theme) {
+        return null;
+    }
+
+    @Override
+    public void delete(long id) {
+
+    }
+}
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
index ddb8faa35b..400186c4b1 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
@@ -12,10 +12,13 @@
 import org.springframework.jdbc.core.JdbcTemplate;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 @SpringBootTest
 class JdbcTemplateReservationRepositoryTest {
     private static final ReservationTime DEFAULT_TIME = new ReservationTime(1L, LocalTime.of(11, 56));
+    private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "썸네일");
+
     @Autowired
     private ReservationRepository reservationRepository;
     @Autowired
@@ -34,7 +37,8 @@ void init() {
     @DisplayName("Reservation 을 잘 저장하는지 확인한다.")
     void save() {
         var beforeSave = reservationRepository.findAll();
-        Reservation saved = reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME));
+        Reservation saved = reservationRepository.save(
+                new Reservation("test", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME));
         var afterSave = reservationRepository.findAll();
 
         Assertions.assertThat(afterSave)
@@ -46,8 +50,8 @@ void save() {
     @DisplayName("Reservation 을 잘 조회하는지 확인한다.")
     void findAll() {
         List<Reservation> beforeSave = reservationRepository.findAll();
-        reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME));
-        reservationRepository.save(new Reservation("test2", LocalDate.now(), DEFAULT_TIME));
+        reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME));
+        reservationRepository.save(new Reservation("test2", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME));
 
         List<Reservation> afterSave = reservationRepository.findAll();
         Assertions.assertThat(afterSave.size())
@@ -58,7 +62,7 @@ void findAll() {
     @DisplayName("Reservation 을 잘 지우는지 확인한다.")
     void delete() {
         List<Reservation> beforeSaveAndDelete = reservationRepository.findAll();
-        reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME));
+        reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME));
 
         reservationRepository.delete(1L);
 

From af7f7e670b9675fbb73817e77f2a123b017cf72f Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 01:10:22 +0900
Subject: [PATCH 24/75] =?UTF-8?q?docs:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?=
 =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 이미 구현된 기능의 체크 리스트 누락된 것 작성

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 1d935687fd..34a3f53a0e 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@
     - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [x] 지나간 날짜와 시간의 예약 요청 시 에러
     - [x] 이름이 비어있는 예약 요청 시 에러
-    - [ ] 존재하지 않는 테마 예약 생성 요청시 에러
-    - [ ] 테마 값이 비어있는 예약 요청 시 에러
+    - [x] 존재하지 않는 테마 예약 생성 요청시 에러
+    - [x] 테마 값이 비어있는 예약 요청 시 에러
 
 - [ ] 테마에 대한 제약 조건 추가
     - [x] 테마 이름, 설명, 썸네일 이미자가 비어 있을 경우 에러

From 31df8bd3a67a04785411106e4d8b6ecab0d4c885 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 01:10:59 +0900
Subject: [PATCH 25/75] =?UTF-8?q?docs:=20=EC=9D=B4=EB=AF=B8=20=ED=95=B4?=
 =?UTF-8?q?=EA=B2=B0=ED=95=9C=20Todo=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 src/main/java/roomescape/service/ReservationService.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 2980fdae95..8ee1c730e1 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -47,7 +47,6 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
                 reservationRequest.date(),
-                //TODO : 커스텀 예외 사용할지 고민해보기
                 reservationTime,
                 theme
         );

From 564ecaf17cc89e378317d8bd1a5454a29ee9d82e Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 01:17:40 +0900
Subject: [PATCH 26/75] =?UTF-8?q?docs:=20=EC=9D=B4=EB=AF=B8=20=ED=95=B4?=
 =?UTF-8?q?=EA=B2=B0=ED=95=9C=20Todo=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                            |  4 ++--
 src/main/java/roomescape/domain/Theme.java           |  1 +
 .../java/roomescape/exception/ExceptionType.java     |  2 ++
 src/main/java/roomescape/service/ThemeService.java   | 12 +++++++++++-
 4 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 34a3f53a0e..dd95dddb2e 100644
--- a/README.md
+++ b/README.md
@@ -14,10 +14,10 @@
     - [x] 존재하지 않는 테마 예약 생성 요청시 에러
     - [x] 테마 값이 비어있는 예약 요청 시 에러
 
-- [ ] 테마에 대한 제약 조건 추가
+- [x] 테마에 대한 제약 조건 추가
     - [x] 테마 이름, 설명, 썸네일 이미자가 비어 있을 경우 에러
     - [x] 중복된 이름의 테마 생성 요청시 에러
-    - [ ] 예약이 있는 테마를 삭제 요청시 에러
+    - [x] 예약이 있는 테마를 삭제 요청시 에러
 
 # API 명세
 
diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index 407b60c7f6..38f6701881 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -46,6 +46,7 @@ public Theme(String name, String description, String thumbnail) {
         this(null, name, description, thumbnail);
     }
 
+    //todo : long 으로 변경
     public Long getId() {
         return id;
     }
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index 038a88ce56..51319d5b25 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -4,6 +4,7 @@
 
 import org.springframework.http.HttpStatus;
 
+//Todo 상태코드 고민해보기
 public enum ExceptionType {
 
     NAME_EMPTY(BAD_REQUEST, "이름은 필수 값입니다."),
@@ -14,6 +15,7 @@ public enum ExceptionType {
     INVALID_DATE_TIME_FORMAT(BAD_REQUEST, "해석할 수 없는 날짜, 시간 포맷입니다."),
     //Todo 이름 변경
     INVALID_DELETE_TIME(BAD_REQUEST, "예약이 존재하는 시간은 삭제할 수 없습니다."),
+    INVALID_DELETE_THEME(BAD_REQUEST, "예약이 존재하는 테마는 삭제할 수 없습니다."),
     RESERVATION_TIME_NOT_FOUND(BAD_REQUEST, "존재하지 않는 시간입니다."),
     //todo 이름 변경,
     PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index f62507dfd3..3fd8f6bab1 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -1,12 +1,14 @@
 package roomescape.service;
 
 import java.util.List;
+import java.util.Objects;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Theme;
 import roomescape.dto.ThemeRequest;
 import roomescape.dto.ThemeResponse;
 import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
+import roomescape.repository.ReservationRepository;
 import roomescape.repository.ThemeRepository;
 
 //todo 테스트코드 작성
@@ -14,9 +16,11 @@
 public class ThemeService {
 
     private final ThemeRepository themeRepository;
+    private final ReservationRepository reservationRepository;
 
-    public ThemeService(ThemeRepository themeRepository) {
+    public ThemeService(ThemeRepository themeRepository, ReservationRepository reservationRepository) {
         this.themeRepository = themeRepository;
+        this.reservationRepository = reservationRepository;
     }
 
     public ThemeResponse save(ThemeRequest themeRequest) {
@@ -41,6 +45,12 @@ public List<ThemeResponse> findAll() {
     }
 
     public void delete(long id) {
+        //todo : 변수명 고민
+        boolean invalidDelete = reservationRepository.findAll().stream()
+                .anyMatch(reservation -> Objects.equals(reservation.getTheme().getId(), id));
+        if (invalidDelete) {
+            throw new RoomescapeException(ExceptionType.INVALID_DELETE_THEME);
+        }
         themeRepository.delete(id);
     }
 }

From 550f4cd78b7bc9a63ba262ca636387794c384961 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 01:26:40 +0900
Subject: [PATCH 27/75] =?UTF-8?q?fix:=20equals=20=EB=A9=94=EC=84=9C?=
 =?UTF-8?q?=EB=93=9C=EA=B0=80=20id=EB=A7=8C=EC=9C=BC=EB=A1=9C=20=EB=B9=84?=
 =?UTF-8?q?=EA=B5=90=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 .../java/roomescape/domain/Reservation.java   | 11 +---------
 .../roomescape/domain/ReservationTime.java    |  5 +----
 src/main/java/roomescape/domain/Theme.java    | 20 +++++++++++++++++++
 3 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index a5d2c3e45d..a157423a48 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -90,16 +90,7 @@ public boolean equals(Object o) {
 
         Reservation that = (Reservation) o;
 
-        if (!Objects.equals(id, that.id)) {
-            return false;
-        }
-        if (!Objects.equals(name, that.name)) {
-            return false;
-        }
-        if (!Objects.equals(date, that.date)) {
-            return false;
-        }
-        return Objects.equals(time, that.time);
+        return Objects.equals(id, that.id);
     }
 
     @Override
diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index 847a933d2c..da26cf0c89 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -49,10 +49,7 @@ public boolean equals(Object o) {
 
         ReservationTime that = (ReservationTime) o;
 
-        if (!Objects.equals(id, that.id)) {
-            return false;
-        }
-        return Objects.equals(startAt, that.startAt);
+        return Objects.equals(id, that.id);
     }
 
     @Override
diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index 38f6701881..0134804dbb 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -1,5 +1,6 @@
 package roomescape.domain;
 
+import java.util.Objects;
 import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 
@@ -62,4 +63,23 @@ public String getDescription() {
     public String getThumbnail() {
         return thumbnail;
     }
+
+    @Override
+    public int hashCode() {
+        return id != null ? id.hashCode() : 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Theme theme = (Theme) o;
+
+        return Objects.equals(id, theme.id);
+    }
 }

From d1c9bee0ca00fce245a120f07c7f9f05e4968266 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 01:27:09 +0900
Subject: [PATCH 28/75] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EC=98=88?=
 =?UTF-8?q?=EC=95=BD=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=B4=20?=
 =?UTF-8?q?=ED=85=8C=EB=A7=88=EB=8F=84=20=ED=99=95=EC=9D=B8=ED=95=98?=
 =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md                                           |  2 +-
 .../java/roomescape/service/ReservationService.java | 13 +++++++++----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index dd95dddb2e..eb88573794 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
     - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러
     - [x] 예약이 있는 예약 시간을 삭제 요청 시 에러
 - [x] 예약에 대한 제약 조건 추가
-    - [x] 동일한 날짜와 시간에 예약 생성 요청 시 에러
+    - [x] 동일한 날짜와 시간, 테마에 예약 생성 요청 시 에러
     - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러
     - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러
     - [x] 지나간 날짜와 시간의 예약 요청 시 에러
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 8ee1c730e1..4d76b6254b 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -52,7 +52,7 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         );
         boolean isDuplicate = reservationRepository.findAll()
                 .stream()
-                .anyMatch(reservation -> mapToLocalDateTime(reservation).equals(mapToLocalDateTime(beforeSave)));
+                .anyMatch(reservation -> validateDuplicateReservation(beforeSave, reservation));
         if (isDuplicate) {
             throw new RoomescapeException(DUPLICATE_RESERVATION);
         }
@@ -66,9 +66,9 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         return toResponse(saved);
     }
 
-    private static boolean isBefore(Reservation beforeSave) {
-        return LocalDateTime.of(beforeSave.getDate(), beforeSave.getTime())
-                .isBefore(LocalDateTime.now());
+    private boolean validateDuplicateReservation(Reservation beforeSave, Reservation reservation) {
+        return mapToLocalDateTime(reservation).equals(mapToLocalDateTime(beforeSave))
+                && beforeSave.getTheme().equals(reservation.getTheme());
     }
 
     private LocalDateTime mapToLocalDateTime(Reservation reservation) {
@@ -77,6 +77,11 @@ private LocalDateTime mapToLocalDateTime(Reservation reservation) {
         return LocalDateTime.of(date, time);
     }
 
+    private static boolean isBefore(Reservation beforeSave) {
+        return LocalDateTime.of(beforeSave.getDate(), beforeSave.getTime())
+                .isBefore(LocalDateTime.now());
+    }
+
     private ReservationResponse toResponse(Reservation reservation) {
         ReservationTime reservationTime = reservation.getReservationTime();
         ReservationTimeResponse reservationTimeResponse = new ReservationTimeResponse(reservationTime.getId(),

From 1cfec9a404d2f0e42a40fae26ff4e5f9ea6ff94a Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 02:58:35 +0900
Subject: [PATCH 29/75] =?UTF-8?q?docs:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?=
 =?UTF-8?q?=EC=98=88=EC=95=BD=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20API=20?=
 =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 README.md | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/README.md b/README.md
index eb88573794..7c7b3324bd 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,8 @@
     - [x] 중복된 이름의 테마 생성 요청시 에러
     - [x] 예약이 있는 테마를 삭제 요청시 에러
 
+- [ ] 사용자 예약 기능 추가
+
 # API 명세
 
 ## 예약 조회 API
@@ -222,3 +224,24 @@
 ### response
 
 > HTTP/1.1 204
+
+## 예약 가능 시간 조회 API
+
+### Request
+
+> GET /availableTimes?date=${date}&themeId=${themeId}
+
+### response
+
+> HTTP/1.1 200
+> Content-Type: application/json
+
+```json
+[
+  {
+    "id": 0,
+    "startAt": "02:53",
+    "isBooked": false
+  }
+]
+```

From 37c3bc124d174a52893af5ebe81fccfc36c9b789 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Wed, 1 May 2024 02:58:57 +0900
Subject: [PATCH 30/75] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?=
 =?UTF-8?q?=EC=98=88=EC=95=BD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: zangsu <zangsu_@naver.com>
---
 .../controller/AvailableTimeController.java   | 26 +++++++++++
 .../roomescape/controller/UserController.java | 12 ++++++
 .../roomescape/dto/AvailableTimeResponse.java |  6 +++
 .../service/AvailableTimeService.java         | 43 +++++++++++++++++++
 .../resources/static/js/user-reservation.js   | 14 +++---
 5 files changed, 94 insertions(+), 7 deletions(-)
 create mode 100644 src/main/java/roomescape/controller/AvailableTimeController.java
 create mode 100644 src/main/java/roomescape/controller/UserController.java
 create mode 100644 src/main/java/roomescape/dto/AvailableTimeResponse.java
 create mode 100644 src/main/java/roomescape/service/AvailableTimeService.java

diff --git a/src/main/java/roomescape/controller/AvailableTimeController.java b/src/main/java/roomescape/controller/AvailableTimeController.java
new file mode 100644
index 0000000000..ffc8f65829
--- /dev/null
+++ b/src/main/java/roomescape/controller/AvailableTimeController.java
@@ -0,0 +1,26 @@
+package roomescape.controller;
+
+import java.time.LocalDate;
+import java.util.List;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import roomescape.dto.AvailableTimeResponse;
+import roomescape.service.AvailableTimeService;
+
+@RestController
+@RequestMapping("/availableTimes")
+public class AvailableTimeController {
+
+    private final AvailableTimeService availableTimeService;
+
+    public AvailableTimeController(AvailableTimeService availableTimeService) {
+        this.availableTimeService = availableTimeService;
+    }
+
+    @GetMapping()
+    public List<AvailableTimeResponse> findByThemeAndDate(@RequestParam LocalDate date, @RequestParam long themeId) {
+        return availableTimeService.findByThemeAndDate(date, themeId);
+    }
+}
diff --git a/src/main/java/roomescape/controller/UserController.java b/src/main/java/roomescape/controller/UserController.java
new file mode 100644
index 0000000000..39d95139d3
--- /dev/null
+++ b/src/main/java/roomescape/controller/UserController.java
@@ -0,0 +1,12 @@
+package roomescape.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class UserController {
+    @GetMapping("/reservation")
+    public String reservationPage() {
+        return "reservation";
+    }
+}
diff --git a/src/main/java/roomescape/dto/AvailableTimeResponse.java b/src/main/java/roomescape/dto/AvailableTimeResponse.java
new file mode 100644
index 0000000000..8297b1dc4f
--- /dev/null
+++ b/src/main/java/roomescape/dto/AvailableTimeResponse.java
@@ -0,0 +1,6 @@
+package roomescape.dto;
+
+import java.time.LocalTime;
+
+public record AvailableTimeResponse(long id, LocalTime startAt, boolean isBooked) {
+}
diff --git a/src/main/java/roomescape/service/AvailableTimeService.java b/src/main/java/roomescape/service/AvailableTimeService.java
new file mode 100644
index 0000000000..07b90674cc
--- /dev/null
+++ b/src/main/java/roomescape/service/AvailableTimeService.java
@@ -0,0 +1,43 @@
+package roomescape.service;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Service;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.dto.AvailableTimeResponse;
+import roomescape.repository.ReservationRepository;
+import roomescape.repository.ReservationTimeRepository;
+
+@Service
+public class AvailableTimeService {
+    private final ReservationRepository reservationRepository;
+    private final ReservationTimeRepository reservationTimeRepository;
+
+    public AvailableTimeService(ReservationRepository reservationRepository,
+                                ReservationTimeRepository reservationTimeRepository) {
+        this.reservationRepository = reservationRepository;
+        this.reservationTimeRepository = reservationTimeRepository;
+    }
+
+    //todo : 메서드 개선
+    public List<AvailableTimeResponse> findByThemeAndDate(LocalDate date, long themeId) {
+        Set<Long> alreadyReservedTimeIds = reservationRepository.findAll().stream()
+                .filter(reservation -> reservation.getDate().equals(date))
+                //todo Reservation에 메서드 구현하기
+                .filter(reservation -> reservation.getTheme().getId().equals(themeId))
+                .map(Reservation::getReservationTime)
+                .map(ReservationTime::getId)
+                .collect(Collectors.toSet());
+
+        return reservationTimeRepository.findAll().stream()
+                .map(reservationTime -> {
+                    long id = reservationTime.getId();
+                    boolean isBooked = alreadyReservedTimeIds.contains(id);
+                    return new AvailableTimeResponse(id, reservationTime.getStartAt(), isBooked);
+                })
+                .toList();
+    }
+}
diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js
index 89ff141af8..a0bcaed178 100644
--- a/src/main/resources/static/js/user-reservation.js
+++ b/src/main/resources/static/js/user-reservation.js
@@ -36,8 +36,8 @@ function renderTheme(themes) {
   const themeSlots = document.getElementById('theme-slots');
   themeSlots.innerHTML = '';
   themes.forEach(theme => {
-    const name = '';
-    const themeId = '';
+    const name = theme.name;
+    const themeId = theme.id;
     /*
     TODO: [3단계] 사용자 예약 - 테마 목록 조회 API 호출 후 렌더링
           response 명세에 맞춰 createSlot 함수 호출 시 값 설정
@@ -91,7 +91,7 @@ function fetchAvailableTimes(date, themeId) {
   TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출
         요청 포맷에 맞게 설정
   */
-  fetch('/', { // 예약 가능 시간 조회 API endpoint
+  fetch(`/availableTimes?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
     method: 'GET',
     headers: {
       'Content-Type': 'application/json',
@@ -100,7 +100,7 @@ function fetchAvailableTimes(date, themeId) {
     if (response.status === 200) return response.json();
     throw new Error('Read failed');
   }).then(renderAvailableTimes)
-  .catch(error => console.error("Error fetching available times:", error));
+      .catch(error => console.error("Error fetching available times:", error));
 }
 
 function renderAvailableTimes(times) {
@@ -120,9 +120,9 @@ function renderAvailableTimes(times) {
     TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 후 렌더링
           response 명세에 맞춰 createSlot 함수 호출 시 값 설정
     */
-    const startAt = '';
-    const timeId = '';
-    const alreadyBooked = false;
+    const startAt = time.startAt;
+    const timeId = time.id;
+    const alreadyBooked = time.isBooked;
 
     const div = createSlot('time', startAt, timeId, alreadyBooked); // createSlot('time', 시작 시간, time id, 예약 여부)
     timeSlots.appendChild(div);

From cb113fdbede005484ebf6334e35d1757ca371506 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 16:58:34 +0900
Subject: [PATCH 31/75] =?UTF-8?q?test=20:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../roomescape/domain/ReservationTime.java    |  1 -
 .../roomescape/domain/ReservationTest.java    | 14 +++++
 .../domain/ReservationTimeTest.java           | 37 ++++++++++++
 .../java/roomescape/domain/ThemeTest.java     | 58 +++++++++++++++++++
 4 files changed, 109 insertions(+), 1 deletion(-)
 create mode 100644 src/test/java/roomescape/domain/ReservationTimeTest.java
 create mode 100644 src/test/java/roomescape/domain/ThemeTest.java

diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index da26cf0c89..09c20cdd93 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -14,7 +14,6 @@ public ReservationTime(LocalTime startAt) {
         this(null, startAt);
     }
 
-    //TODO 테스트 추가
     public ReservationTime(Long id, LocalTime startAt) {
         if (startAt == null) {
             throw new RoomescapeException(TIME_EMPTY);
diff --git a/src/test/java/roomescape/domain/ReservationTest.java b/src/test/java/roomescape/domain/ReservationTest.java
index 571020049a..cdd5840fb7 100644
--- a/src/test/java/roomescape/domain/ReservationTest.java
+++ b/src/test/java/roomescape/domain/ReservationTest.java
@@ -1,15 +1,29 @@
 package roomescape.domain;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
 import java.time.LocalDate;
 import java.time.LocalTime;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import roomescape.exception.ExceptionType;
+import roomescape.exception.RoomescapeException;
 
 class ReservationTest {
 
+    private static final LocalDate DEFAULT_DATE = LocalDate.now();
+    private static final ReservationTime DEFAULT_TIME = new ReservationTime(1L, LocalTime.now());
     private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "썸네일");
 
+    @DisplayName("생성 테스트")
+    @Test
+    void constructTest() {
+        assertThatThrownBy(() -> new Reservation(null, DEFAULT_DATE, DEFAULT_TIME, DEFAULT_THEME))
+                .isInstanceOf(RoomescapeException.class)
+                .hasMessage(ExceptionType.NAME_EMPTY.getMessage());
+    }
+
     @Test
     @DisplayName("날짜를 기준으로 비교를 잘 하는지 확인.")
     void compareTo() {
diff --git a/src/test/java/roomescape/domain/ReservationTimeTest.java b/src/test/java/roomescape/domain/ReservationTimeTest.java
new file mode 100644
index 0000000000..e822f5646b
--- /dev/null
+++ b/src/test/java/roomescape/domain/ReservationTimeTest.java
@@ -0,0 +1,37 @@
+package roomescape.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.time.LocalTime;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import roomescape.exception.ExceptionType;
+import roomescape.exception.RoomescapeException;
+
+class ReservationTimeTest {
+
+    @DisplayName("시간이 null 인 ReservationTime 을 생성할 수 없다.")
+    @Test
+    void startAtMustBeNotNull() {
+        assertThatThrownBy(() -> new ReservationTime(null))
+                .isInstanceOf(RoomescapeException.class)
+                .hasMessage(ExceptionType.TIME_EMPTY.getMessage());
+    }
+
+    @DisplayName("ReservationTime 은 id 값으로만 동등성을 비교한다.")
+    @Test
+    void equalsTest() {
+        assertAll(
+                () -> assertThat(new ReservationTime(1L, LocalTime.of(11, 11)))
+                        .isEqualTo(new ReservationTime(1L, LocalTime.of(11, 20))),
+
+                () -> assertThat(new ReservationTime(1L, LocalTime.of(11, 11)))
+                        .isNotEqualTo(new ReservationTime(2L, LocalTime.of(11, 11))),
+
+                () -> assertThat(new ReservationTime(null, LocalTime.of(11, 11)))
+                        .isNotEqualTo(new ReservationTime(1L, LocalTime.of(11, 11)))
+        );
+    }
+}
diff --git a/src/test/java/roomescape/domain/ThemeTest.java b/src/test/java/roomescape/domain/ThemeTest.java
new file mode 100644
index 0000000000..9d63424f52
--- /dev/null
+++ b/src/test/java/roomescape/domain/ThemeTest.java
@@ -0,0 +1,58 @@
+package roomescape.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static roomescape.exception.ExceptionType.DESCRIPTION_EMPTY;
+import static roomescape.exception.ExceptionType.NAME_EMPTY;
+import static roomescape.exception.ExceptionType.THUMBNAIL_EMPTY;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import roomescape.exception.RoomescapeException;
+
+class ThemeTest {
+
+    @DisplayName("객체 생성 테스트")
+    @Test
+    void constructTest() {
+        assertAll(
+                () -> assertThatThrownBy(() -> new Theme(null, "description", "thumbnail"))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(NAME_EMPTY.getMessage()),
+
+                () -> assertThatThrownBy(() -> new Theme("name", null, "thumbnail"))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(DESCRIPTION_EMPTY.getMessage()),
+
+                () -> assertThatThrownBy(() -> new Theme("name", "description", null))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(THUMBNAIL_EMPTY.getMessage()),
+
+                () -> assertThatCode(() -> new Theme("name", "description", "thumbnail"))
+                        .doesNotThrowAnyException(),
+
+                () -> assertThatCode(() -> new Theme(null, "name", "description", "thumbnail"))
+                        .doesNotThrowAnyException(),
+
+                () -> assertThatCode(() -> new Theme(1L, "name", "description", "thumbnail"))
+                        .doesNotThrowAnyException()
+        );
+    }
+
+    @DisplayName("동등성 테스트")
+    @Test
+    void equalsTest() {
+        assertAll(
+                () -> assertThat(new Theme(1L, "name", "description", "thumbnail"))
+                        .isEqualTo(new Theme(1L, "otherName", "otherDescription", "otherThumbnail")),
+
+                () -> assertThat(new Theme(1L, "sameName", "sameDescription", "sameThumbnail"))
+                        .isNotEqualTo(new Theme(2L, "sameName", "sameDescription", "sameThumbnail")),
+
+                () -> assertThat(new Theme(1L, "sameName", "sameDescription", "sameThumbnail"))
+                        .isNotEqualTo(new Theme(null, "sameName", "sameDescription", "sameThumbnail"))
+        );
+    }
+}

From 42c24154ae72d1c5b95708a3671eb1210babd9de Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 17:24:57 +0900
Subject: [PATCH 32/75] =?UTF-8?q?refactor=20:=20=EB=8F=84=EB=A9=94?=
 =?UTF-8?q?=EC=9D=B8=EC=97=90=EA=B2=8C=20=EC=9C=84=EC=9E=84=ED=95=A0=20?=
 =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20?=
 =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?=
 =?UTF-8?q?=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/roomescape/domain/Reservation.java   | 21 +++++++++++++++++++
 .../roomescape/domain/ReservationTime.java    |  4 ++++
 src/main/java/roomescape/domain/Theme.java    |  8 +++++++
 .../service/AvailableTimeService.java         |  5 ++---
 .../service/ReservationService.java           | 11 +++-------
 .../service/ReservationTimeService.java       |  2 +-
 .../java/roomescape/service/ThemeService.java |  4 ++--
 7 files changed, 41 insertions(+), 14 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index a157423a48..aaa3a1744d 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -46,6 +46,27 @@ public boolean hasSameId(long id) {
         return this.id == id;
     }
 
+    public boolean isReservationTimeOf(long id) {
+        return this.time.isIdOf(id);
+    }
+
+    public boolean isDateOf(LocalDate date) {
+        return this.date.equals(date);
+    }
+
+    public boolean isThemeOf(long id) {
+        return this.theme.isIdOf(id);
+    }
+
+    public boolean isSameDateTime(Reservation beforeSave) {
+        return LocalDateTime.of(this.date, this.getTime())
+                .equals(LocalDateTime.of(beforeSave.date, beforeSave.getTime()));
+    }
+
+    public boolean isSameTheme(Reservation reservation) {
+        return this.theme.equals(reservation.theme);
+    }
+
     public long getId() {
         return id;
     }
diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index 09c20cdd93..bfd1bd8763 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -22,6 +22,10 @@ public ReservationTime(Long id, LocalTime startAt) {
         this.startAt = startAt;
     }
 
+    public boolean isIdOf(long id) {
+        return this.id == id;
+    }
+
     public Long getId() {
         return id;
     }
diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index 0134804dbb..cc7ada5a3a 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -47,6 +47,14 @@ public Theme(String name, String description, String thumbnail) {
         this(null, name, description, thumbnail);
     }
 
+    public boolean isIdOf(long id) {
+        return this.id == id;
+    }
+
+    public boolean isNameOf(String name) {
+        return this.name.equals(name);
+    }
+
     //todo : long 으로 변경
     public Long getId() {
         return id;
diff --git a/src/main/java/roomescape/service/AvailableTimeService.java b/src/main/java/roomescape/service/AvailableTimeService.java
index 07b90674cc..909c6ad493 100644
--- a/src/main/java/roomescape/service/AvailableTimeService.java
+++ b/src/main/java/roomescape/service/AvailableTimeService.java
@@ -25,9 +25,8 @@ public AvailableTimeService(ReservationRepository reservationRepository,
     //todo : 메서드 개선
     public List<AvailableTimeResponse> findByThemeAndDate(LocalDate date, long themeId) {
         Set<Long> alreadyReservedTimeIds = reservationRepository.findAll().stream()
-                .filter(reservation -> reservation.getDate().equals(date))
-                //todo Reservation에 메서드 구현하기
-                .filter(reservation -> reservation.getTheme().getId().equals(themeId))
+                .filter(reservation -> reservation.isDateOf(date))
+                .filter(reservation -> reservation.isThemeOf(themeId))
                 .map(Reservation::getReservationTime)
                 .map(ReservationTime::getId)
                 .collect(Collectors.toSet());
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 4d76b6254b..f8415a526f 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -67,16 +67,11 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
     }
 
     private boolean validateDuplicateReservation(Reservation beforeSave, Reservation reservation) {
-        return mapToLocalDateTime(reservation).equals(mapToLocalDateTime(beforeSave))
-                && beforeSave.getTheme().equals(reservation.getTheme());
-    }
-
-    private LocalDateTime mapToLocalDateTime(Reservation reservation) {
-        LocalDate date = reservation.getDate();
-        LocalTime time = reservation.getReservationTime().getStartAt();
-        return LocalDateTime.of(date, time);
+        return reservation.isSameDateTime(beforeSave)
+                && beforeSave.isSameTheme(reservation);
     }
 
+    //TODO : 도메인에게 넘길 수 있을 것 같은데
     private static boolean isBefore(Reservation beforeSave) {
         return LocalDateTime.of(beforeSave.getDate(), beforeSave.getTime())
                 .isBefore(LocalDateTime.now());
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 0b1b97b5c8..74db95e3a9 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -49,7 +49,7 @@ public void delete(long id) {
         List<Reservation> reservations = reservationRepository.findAll();
         //TODO : 지역변수 네이밍 고민
         boolean invalidDelete = reservations.stream()
-                .anyMatch(reservation -> reservation.getReservationTime().getId() == id);
+                .anyMatch(reservation -> reservation.isReservationTimeOf(id));
         if (invalidDelete) {
             throw new RoomescapeException(INVALID_DELETE_TIME);
         }
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index 3fd8f6bab1..8d1cea7d18 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -25,7 +25,7 @@ public ThemeService(ThemeRepository themeRepository, ReservationRepository reser
 
     public ThemeResponse save(ThemeRequest themeRequest) {
         boolean hasDuplicateTheme = themeRepository.findAll().stream()
-                .anyMatch(theme -> theme.getName().equals(themeRequest.name()));
+                .anyMatch(theme -> theme.isNameOf(themeRequest.name()));
         if (hasDuplicateTheme) {
             throw new RoomescapeException(ExceptionType.DUPLICATE_THEME);
         }
@@ -47,7 +47,7 @@ public List<ThemeResponse> findAll() {
     public void delete(long id) {
         //todo : 변수명 고민
         boolean invalidDelete = reservationRepository.findAll().stream()
-                .anyMatch(reservation -> Objects.equals(reservation.getTheme().getId(), id));
+                .anyMatch(reservation -> reservation.isThemeOf(id));
         if (invalidDelete) {
             throw new RoomescapeException(ExceptionType.INVALID_DELETE_THEME);
         }

From 8a42260ce8b000596508465dbc8d643ac7ed3e9a Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 20:21:24 +0900
Subject: [PATCH 33/75] =?UTF-8?q?test=20:=20ThemeService=20=EB=8B=A8?=
 =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/roomescape/service/ThemeService.java |   2 -
 .../CollectionReservationRepository.java      |   5 +-
 .../repository/CollectionThemeRepository.java |  23 +++-
 .../roomescape/service/ThemeServiceTest.java  | 117 ++++++++++++++++++
 4 files changed, 140 insertions(+), 7 deletions(-)
 create mode 100644 src/test/java/roomescape/service/ThemeServiceTest.java

diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index 8d1cea7d18..4daac439e3 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -1,7 +1,6 @@
 package roomescape.service;
 
 import java.util.List;
-import java.util.Objects;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Theme;
 import roomescape.dto.ThemeRequest;
@@ -11,7 +10,6 @@
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ThemeRepository;
 
-//todo 테스트코드 작성
 @Service
 public class ThemeService {
 
diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java
index 213022259a..ca6daa0aa2 100644
--- a/src/test/java/roomescape/repository/CollectionReservationRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java
@@ -1,5 +1,7 @@
 package roomescape.repository;
 
+import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
@@ -7,6 +9,7 @@
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
 import roomescape.dto.ReservationRequest;
+import roomescape.exception.RoomescapeException;
 
 public class CollectionReservationRepository implements ReservationRepository {
     private final List<Reservation> reservations;
@@ -39,7 +42,7 @@ public Reservation save(Reservation reservation) {
         ReservationTime findTime = timeRepository.findAll().stream()
                 .filter(reservationTime -> reservationTime.getId() == reservation.getReservationTime().getId())
                 .findFirst()
-                .get();
+                .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
         Reservation saved = new Reservation(atomicLong.incrementAndGet(), reservation.getName(), reservation.getDate(),
                 findTime, reservation.getTheme());
         reservations.add(saved);
diff --git a/src/test/java/roomescape/repository/CollectionThemeRepository.java b/src/test/java/roomescape/repository/CollectionThemeRepository.java
index a851648ec3..d234bb8f5d 100644
--- a/src/test/java/roomescape/repository/CollectionThemeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionThemeRepository.java
@@ -1,27 +1,42 @@
 package roomescape.repository;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
 import roomescape.domain.Theme;
 
 public class CollectionThemeRepository implements ThemeRepository {
+
+    private final List<Theme> themes;
+    private final AtomicLong index;
+
+    public CollectionThemeRepository() {
+        themes = new ArrayList<>();
+        index = new AtomicLong(0);
+    }
+
     @Override
     public List<Theme> findAll() {
-        return null;
+        return new ArrayList<>(themes);
     }
 
     @Override
     public Optional<Theme> findById(long id) {
-        return Optional.of(new Theme(id, "이름", "설명", "썸네일"));
+        return themes.stream()
+                .filter(theme -> theme.isIdOf(id))
+                .findFirst();
     }
 
     @Override
     public Theme save(Theme theme) {
-        return null;
+        Theme saved = new Theme(index.incrementAndGet(), theme);
+        themes.add(saved);
+        return saved;
     }
 
     @Override
     public void delete(long id) {
-
+        themes.removeIf(theme -> theme.isIdOf(id));
     }
 }
diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java
new file mode 100644
index 0000000000..45d32c1856
--- /dev/null
+++ b/src/test/java/roomescape/service/ThemeServiceTest.java
@@ -0,0 +1,117 @@
+package roomescape.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static roomescape.exception.ExceptionType.DUPLICATE_THEME;
+import static roomescape.exception.ExceptionType.INVALID_DELETE_THEME;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
+import roomescape.dto.ThemeRequest;
+import roomescape.dto.ThemeResponse;
+import roomescape.exception.RoomescapeException;
+import roomescape.repository.CollectionReservationRepository;
+import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.CollectionThemeRepository;
+import roomescape.repository.ReservationRepository;
+import roomescape.repository.ThemeRepository;
+
+class ThemeServiceTest {
+
+    private ThemeRepository themeRepository;
+    private CollectionReservationTimeRepository reservationTimeRepository;
+    private ReservationRepository reservationRepository;
+    private ThemeService themeService;
+
+    @BeforeEach
+    void initService() {
+        themeRepository = new CollectionThemeRepository();
+        reservationTimeRepository = new CollectionReservationTimeRepository();
+        reservationRepository = new CollectionReservationRepository(reservationTimeRepository);
+        themeService = new ThemeService(themeRepository, reservationRepository);
+    }
+
+    @DisplayName("테마, 시간이 하나 존재할 때")
+    @Nested
+    class OneThemeTest {
+        private ReservationTime defaultTime = new ReservationTime(LocalTime.now().plusMinutes(5));
+        private Theme defaultTheme = new Theme("name", "description", "thumbnail");
+
+        @BeforeEach
+        void addDefaultData() {
+            defaultTime = reservationTimeRepository.save(defaultTime);
+            defaultTheme = themeRepository.save(defaultTheme);
+        }
+
+        @DisplayName("동일한 이름의 테마를 예약할 수 없다.")
+        @Test
+        void duplicatedThemeSaveFailTest() {
+            assertThatThrownBy(() -> themeService.save(new ThemeRequest(
+                    defaultTheme.getName(), "description", "thumbnail"
+            ))).isInstanceOf(RoomescapeException.class)
+                    .hasMessage(DUPLICATE_THEME.getMessage());
+        }
+
+        @DisplayName("다른 이름의 테마를 예약할 수 있다.")
+        @Test
+        void notDuplicatedThemeNameSaveTest() {
+            themeService.save(new ThemeRequest("otherName", "description", "thumbnail"));
+
+            assertThat(themeRepository.findAll())
+                    .hasSize(2);
+        }
+
+        @DisplayName("테마에 예약이 없다면 테마를 삭제할 수 있다.")
+        @Test
+        void removeSuccessTest() {
+
+            themeService.delete(1L);
+            assertThat(themeRepository.findById(1L)).isEmpty();
+        }
+
+        @DisplayName("테마에 예약이 있다면 테마를 삭제할 수 없다.")
+        @Test
+        void removeFailTest() {
+            //given
+            reservationRepository.save(new Reservation(
+                    "name", LocalDate.now().plusDays(1), defaultTime, defaultTheme));
+
+            //when & then
+            assertThatThrownBy(() -> themeService.delete(1L))
+                    .isInstanceOf(RoomescapeException.class)
+                    .hasMessage(INVALID_DELETE_THEME.getMessage());
+        }
+
+        @DisplayName("존재하지 않는 테마 id로 삭제하더라도 오류로 간주하지 않는다.")
+        @Test
+        void notExistThemeDeleteTest() {
+            assertThatCode(() -> themeService.delete(2L))
+                    .doesNotThrowAnyException();
+        }
+    }
+
+    @DisplayName("테마가 여러개 있으면 테마를 모두 조회할 수 있다.")
+    @Test
+    void findAllTest() {
+        //given
+        themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
+        themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
+        themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
+        themeRepository.save(new Theme("name4", "description4", "thumbnail4"));
+
+        //when
+        List<ThemeResponse> themeResponses = themeService.findAll();
+
+        //then
+        assertThat(themeResponses).hasSize(4);
+    }
+}

From 584df82d6649e19537c729421a96e451c8efa524 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 20:21:45 +0900
Subject: [PATCH 34/75] =?UTF-8?q?style=20:=20=EB=88=84=EB=9D=BD=EB=90=9C?=
 =?UTF-8?q?=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=A0=81=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/roomescape/service/ReservationService.java | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index f8415a526f..0b99e7e072 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -4,9 +4,7 @@
 import static roomescape.exception.ExceptionType.PAST_TIME;
 import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
 
-import java.time.LocalDate;
 import java.time.LocalDateTime;
-import java.time.LocalTime;
 import java.util.List;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Reservation;

From f6966e194536ce045196012cd7117717bb2d2549 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 20:45:30 +0900
Subject: [PATCH 35/75] =?UTF-8?q?refactor=20:=20=EA=B8=B0=EC=A1=B4?=
 =?UTF-8?q?=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/ReservationControllerTest.java | 80 ++++++++++---------
 .../service/ReservationTimeServiceTest.java   | 16 ++--
 2 files changed, 52 insertions(+), 44 deletions(-)

diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index cbba143c3e..ee1b61be86 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -3,10 +3,10 @@
 import java.lang.reflect.Field;
 import java.time.LocalDate;
 import java.time.LocalTime;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import roomescape.domain.Reservation;
@@ -23,64 +23,72 @@
 
 //TODO : 전체적으로 테스트 수정
 class ReservationControllerTest {
-    static final long timeId = 1L;
-    static final LocalTime time = LocalTime.now();
-    private static final Theme DEFUALT_THEME = new Theme(1L, "이름", "설명", "썸네일");
-    private final CollectionReservationTimeRepository timeRepository = new CollectionReservationTimeRepository(
-            new ArrayList<>(List.of(new ReservationTime(timeId, time)))
-    );
-    private final CollectionThemeRepository themeRepository = new CollectionThemeRepository();
+    private static final long TIME_ID = 1L;
+    private static final LocalTime TIME = LocalTime.now();
+    private ReservationTime defaultTime = new ReservationTime(TIME_ID, TIME);
+    private Theme defualtTheme = new Theme("name", "description", "thumbnail");
+
+    private CollectionReservationRepository collectionReservationRepository;
+    private ReservationController reservationController;
+
+    @BeforeEach
+    void initController() {
+        CollectionReservationTimeRepository timeRepository = new CollectionReservationTimeRepository();
+        CollectionThemeRepository themeRepository = new CollectionThemeRepository();
+        collectionReservationRepository = new CollectionReservationRepository(timeRepository);
+        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
+                themeRepository);
+        reservationController = new ReservationController(reservationService);
+
+        defaultTime = timeRepository.save(defaultTime);
+        defualtTheme = themeRepository.save(defualtTheme);
+    }
 
     @Test
     @DisplayName("예약 정보를 잘 저장하는지 확인한다.")
     void saveReservation() {
-        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
-                timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
-                themeRepository);
-        ReservationController reservationController = new ReservationController(reservationService);
+        //given
         LocalDate date = LocalDate.now().plusDays(1);
 
+        //when
         ReservationResponse saveResponse = reservationController.saveReservation(
-                        new ReservationRequest(date, "폴라", timeId, 1))
+                        new ReservationRequest(date, "폴라", TIME_ID, defualtTheme.getId()))
                 .getBody();
 
         long id = Objects.requireNonNull(saveResponse).id();
+
+        //then
         ReservationResponse expected = new ReservationResponse(id, "폴라", date,
-                new ReservationTimeResponse(timeId, time), new ThemeResponse(1, "이름", "설명", "썸네일"));
+                new ReservationTimeResponse(TIME_ID, TIME),
+                //Todo : [로빈] Response 가 변환 로직을 가지고 있으면 아래 코드도 간단해 질 것 같음
+                new ThemeResponse(defualtTheme.getId(), defualtTheme.getName(), defualtTheme.getDescription(),
+                        defualtTheme.getThumbnail()));
 
-        Assertions.assertThat(saveResponse)
-                .isEqualTo(expected);
+        Assertions.assertThat(saveResponse).isEqualTo(expected);
     }
 
     @Test
     @DisplayName("예약 정보를 잘 불러오는지 확인한다.")
     void findAllReservations() {
-        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
-                timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
-                null);
-        ReservationController reservationController = new ReservationController(reservationService);
+        //when
         List<ReservationResponse> allReservations = reservationController.findAllReservations();
 
-        Assertions.assertThat(allReservations)
-                .isEmpty();
+        //then
+        Assertions.assertThat(allReservations).isEmpty();
     }
 
     @Test
     @DisplayName("예약 정보를 잘 지우는지 확인한다.")
     void delete() {
-        List<Reservation> reservations = List.of(
-                new Reservation(1L, "폴라", LocalDate.now(), new ReservationTime(LocalTime.now()), DEFUALT_THEME));
-        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
-                new ArrayList<>(reservations), timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
-                null);
-        ReservationController reservationController = new ReservationController(reservationService);
+        //given
+        Reservation saved = collectionReservationRepository.save(
+                new Reservation("폴라", LocalDate.now(), defaultTime, defualtTheme));
 
-        reservationController.delete(1L);
-        List<ReservationResponse> reservationResponses = reservationController.findAllReservations();
+        //when
+        reservationController.delete(saved.getId());
 
+        //then
+        List<ReservationResponse> reservationResponses = reservationController.findAllReservations();
         Assertions.assertThat(reservationResponses)
                 .isEmpty();
     }
@@ -88,12 +96,6 @@ void delete() {
     @Test
     @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.")
     void checkRepositoryDependency() {
-        CollectionReservationRepository collectionReservationRepository = new CollectionReservationRepository(
-                timeRepository);
-        ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
-                null);
-        ReservationController reservationController = new ReservationController(reservationService);
-
         boolean isRepositoryInjected = false;
 
         for (Field field : reservationController.getClass().getDeclaredFields()) {
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index cc622871d0..3865031b59 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -4,6 +4,7 @@
 
 import java.time.LocalTime;
 import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import roomescape.dto.ReservationTimeRequest;
@@ -13,14 +14,19 @@
 
 class ReservationTimeServiceTest {
 
+    private ReservationTimeService reservationTimeService;
+
+    @BeforeEach
+    void initService() {
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        CollectionReservationRepository reservationRepository =
+                new CollectionReservationRepository(reservationTimeRepository);
+        reservationTimeService = new ReservationTimeService(reservationRepository, reservationTimeRepository);
+    }
+
     @Test
     @DisplayName("중복된 시간은 생성할 수 없는지 검증")
     void saveFailCauseDuplicate() {
-        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
-                reservationTimeRepository);
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
-                reservationTimeRepository);
         ReservationTimeRequest reservationTimeRequest = new ReservationTimeRequest(LocalTime.of(10, 0));
         reservationTimeService.save(reservationTimeRequest);
 

From 9adfa8d8c0ce7292c56375c9d4a2a268667c151e Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 21:32:17 +0900
Subject: [PATCH 36/75] =?UTF-8?q?refactor=20:=20Reservation=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=EC=97=90=EC=84=9C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/roomescape/domain/Reservation.java   | 34 +++++++++++++++++--
 .../roomescape/exception/ExceptionType.java   | 10 +++---
 .../roomescape/domain/ReservationTest.java    | 27 ++++++++++++---
 3 files changed, 60 insertions(+), 11 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index aaa3a1744d..6ac260905d 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -1,6 +1,9 @@
 package roomescape.domain;
 
+import static roomescape.exception.ExceptionType.DATE_EMPTY;
 import static roomescape.exception.ExceptionType.NAME_EMPTY;
+import static roomescape.exception.ExceptionType.THEME_EMPTY;
+import static roomescape.exception.ExceptionType.TIME_EMPTY;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -20,9 +23,10 @@ public Reservation(String name, LocalDate date, ReservationTime time, Theme them
     }
 
     public Reservation(Long id, String name, LocalDate date, ReservationTime time, Theme theme) {
-        if (name == null || name.isBlank()) {
-            throw new RoomescapeException(NAME_EMPTY);
-        }
+        validateName(name);
+        validateDate(date);
+        validateTime(time);
+        validateTheme(theme);
         this.id = id;
         this.name = name;
         this.date = date;
@@ -30,6 +34,30 @@ public Reservation(Long id, String name, LocalDate date, ReservationTime time, T
         this.theme = theme;
     }
 
+    private void validateTheme(Theme theme) {
+        if (theme == null) {
+            throw new RoomescapeException(THEME_EMPTY);
+        }
+    }
+
+    private void validateTime(ReservationTime time) {
+        if (time == null) {
+            throw new RoomescapeException(TIME_EMPTY);
+        }
+    }
+
+    private void validateDate(LocalDate date) {
+        if (date == null) {
+            throw new RoomescapeException(DATE_EMPTY);
+        }
+    }
+
+    private void validateName(String name) {
+        if (name == null || name.isBlank()) {
+            throw new RoomescapeException(NAME_EMPTY);
+        }
+    }
+
     public Reservation(long id, Reservation reservationBeforeSave) {
         this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time,
                 reservationBeforeSave.theme);
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index 51319d5b25..7725238da8 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -9,18 +9,20 @@ public enum ExceptionType {
 
     NAME_EMPTY(BAD_REQUEST, "이름은 필수 값입니다."),
     TIME_EMPTY(BAD_REQUEST, "시작 시간은 필수 값입니다."),
+    DATE_EMPTY(BAD_REQUEST, "날짜는 필수값 입니다."),
+    THEME_EMPTY(BAD_REQUEST, "테마는 필수값 입니다."),
+    DESCRIPTION_EMPTY(BAD_REQUEST, "테마 설명은 필수값 입니다."),
+    THUMBNAIL_EMPTY(BAD_REQUEST, "테마 썸네일은 필수값 입니다."),
+    PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
     DUPLICATE_RESERVATION(BAD_REQUEST, "같은 시간에 이미 예약이 존재합니다."),
     DUPLICATE_RESERVATION_TIME(BAD_REQUEST, "이미 예약시간이 존재합니다."),
     DUPLICATE_THEME(BAD_REQUEST, "이미 동일한 테마가 존재합니다."),
     INVALID_DATE_TIME_FORMAT(BAD_REQUEST, "해석할 수 없는 날짜, 시간 포맷입니다."),
-    //Todo 이름 변경
+    //Todo 이름 변경,
     INVALID_DELETE_TIME(BAD_REQUEST, "예약이 존재하는 시간은 삭제할 수 없습니다."),
     INVALID_DELETE_THEME(BAD_REQUEST, "예약이 존재하는 테마는 삭제할 수 없습니다."),
     RESERVATION_TIME_NOT_FOUND(BAD_REQUEST, "존재하지 않는 시간입니다."),
     //todo 이름 변경,
-    PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
-    DESCRIPTION_EMPTY(BAD_REQUEST, "테마 설명은 필수값 입니다."),
-    THUMBNAIL_EMPTY(BAD_REQUEST, "테마 썸네일은 필수값 입니다."),
     THEME_NOT_FOUND(BAD_REQUEST, "없는 테마입니다."),
     ;
 
diff --git a/src/test/java/roomescape/domain/ReservationTest.java b/src/test/java/roomescape/domain/ReservationTest.java
index cdd5840fb7..17b021a41d 100644
--- a/src/test/java/roomescape/domain/ReservationTest.java
+++ b/src/test/java/roomescape/domain/ReservationTest.java
@@ -1,13 +1,17 @@
 package roomescape.domain;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static roomescape.exception.ExceptionType.DATE_EMPTY;
+import static roomescape.exception.ExceptionType.NAME_EMPTY;
+import static roomescape.exception.ExceptionType.THEME_EMPTY;
+import static roomescape.exception.ExceptionType.TIME_EMPTY;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
-import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 
 class ReservationTest {
@@ -19,9 +23,24 @@ class ReservationTest {
     @DisplayName("생성 테스트")
     @Test
     void constructTest() {
-        assertThatThrownBy(() -> new Reservation(null, DEFAULT_DATE, DEFAULT_TIME, DEFAULT_THEME))
-                .isInstanceOf(RoomescapeException.class)
-                .hasMessage(ExceptionType.NAME_EMPTY.getMessage());
+        assertAll(
+                () -> assertThatThrownBy(() -> new Reservation(null, DEFAULT_DATE, DEFAULT_TIME, DEFAULT_THEME))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(NAME_EMPTY.getMessage()),
+
+                () -> assertThatThrownBy(() -> new Reservation("name", null, DEFAULT_TIME, DEFAULT_THEME))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(DATE_EMPTY.getMessage()),
+
+                () -> assertThatThrownBy(() -> new Reservation("name", DEFAULT_DATE, null, DEFAULT_THEME))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(TIME_EMPTY.getMessage()),
+
+                () -> assertThatThrownBy(() -> new Reservation("name", DEFAULT_DATE, DEFAULT_TIME, null))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(THEME_EMPTY.getMessage())
+        );
+
     }
 
     @Test

From 24c83c6801f763cd4374939308bc61fd02f79546 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 21:41:28 +0900
Subject: [PATCH 37/75] =?UTF-8?q?test=20:=20ReservationService=20=EB=8B=A8?=
 =?UTF-8?q?=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/ReservationService.java           |  13 +-
 .../service/ReservationServiceTest.java       | 179 ++++++++++++++++++
 2 files changed, 184 insertions(+), 8 deletions(-)
 create mode 100644 src/test/java/roomescape/service/ReservationServiceTest.java

diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 0b99e7e072..de04fb3a12 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -33,20 +33,18 @@ public ReservationService(ReservationRepository reservationRepository,
         this.themeRepository = themeRepository;
     }
 
-    //ToDo 테스트 작성
     public ReservationResponse save(ReservationRequest reservationRequest) {
-        //TODO 변수명
-        ReservationTime reservationTime = reservationTimeRepository.findById(reservationRequest.timeId())
-                .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
 
-        Theme theme = themeRepository.findById(reservationRequest.themeId())
+        ReservationTime requestedTime = reservationTimeRepository.findById(reservationRequest.timeId())
+                .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
+        Theme requestedTheme = themeRepository.findById(reservationRequest.themeId())
                 .orElseThrow(() -> new RoomescapeException(ExceptionType.THEME_NOT_FOUND));
 
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
                 reservationRequest.date(),
-                reservationTime,
-                theme
+                requestedTime,
+                requestedTheme
         );
         boolean isDuplicate = reservationRepository.findAll()
                 .stream()
@@ -55,7 +53,6 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
             throw new RoomescapeException(DUPLICATE_RESERVATION);
         }
 
-        //todo 테스트
         if (isBefore(beforeSave)) {
             throw new RoomescapeException(PAST_TIME);
         }
diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java
new file mode 100644
index 0000000000..f49356cff9
--- /dev/null
+++ b/src/test/java/roomescape/service/ReservationServiceTest.java
@@ -0,0 +1,179 @@
+package roomescape.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION;
+import static roomescape.exception.ExceptionType.NAME_EMPTY;
+import static roomescape.exception.ExceptionType.PAST_TIME;
+import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
+import static roomescape.exception.ExceptionType.THEME_NOT_FOUND;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
+import roomescape.dto.ReservationRequest;
+import roomescape.dto.ReservationResponse;
+import roomescape.exception.RoomescapeException;
+import roomescape.repository.CollectionReservationRepository;
+import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.CollectionThemeRepository;
+import roomescape.repository.ReservationRepository;
+import roomescape.repository.ThemeRepository;
+
+class ReservationServiceTest {
+
+    private ReservationRepository reservationRepository;
+    private ReservationService reservationService;
+
+    private ReservationTime defaultTime = new ReservationTime(LocalTime.now());
+    private Theme defaultTheme = new Theme("name", "description", "thumbnail");
+
+    @BeforeEach
+    void initService() {
+        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
+        ThemeRepository themeRepository = new CollectionThemeRepository();
+        reservationRepository = new CollectionReservationRepository(reservationTimeRepository);
+        reservationService = new ReservationService(reservationRepository, reservationTimeRepository, themeRepository);
+
+        defaultTime = reservationTimeRepository.save(defaultTime);
+        defaultTheme = themeRepository.save(defaultTheme);
+    }
+
+    @DisplayName("지나지 않은 시간에 대한 예약을 생성할 수 있다.")
+    @Test
+    void createFutureReservationTest() {
+        //when
+        ReservationResponse saved = reservationService.save(new ReservationRequest(
+                LocalDate.now().plusDays(1),
+                "name",
+                defaultTime.getId(),
+                defaultTheme.getId()
+        ));
+
+        //then
+        assertAll(
+                () -> assertThat(reservationRepository.findAll())
+                        .hasSize(1),
+                () -> assertThat(saved.id()).isEqualTo(1L)
+        );
+    }
+
+    @DisplayName("지난 시간에 대해 예약을 시도할 경우 예외가 발생한다.")
+    @Test
+    void createPastReservationFailTest() {
+        assertThatThrownBy(() -> reservationService.save(new ReservationRequest(
+                LocalDate.now().minusDays(1),
+                "name",
+                defaultTime.getId(),
+                defaultTheme.getId()
+        )))
+                .isInstanceOf(RoomescapeException.class)
+                .hasMessage(PAST_TIME.getMessage());
+    }
+
+    @DisplayName("존재하지 않는 시간에 대해 예약을 생성하면 예외가 발생한다.")
+    @Test
+    void createReservationWithTimeNotExistsTest() {
+        assertThatThrownBy(() -> reservationService.save(new ReservationRequest(
+                LocalDate.now().minusDays(1),
+                "name",
+                2L,
+                defaultTheme.getId()
+        )))
+                .isInstanceOf(RoomescapeException.class)
+                .hasMessage(RESERVATION_TIME_NOT_FOUND.getMessage());
+    }
+
+    @DisplayName("존재하지 않는 테마에 대해 예약을 생성하면 예외가 발생한다.")
+    @Test
+    void createReservationWithThemeNotExistsTest() {
+        assertThatThrownBy(() -> reservationService.save(new ReservationRequest(
+                LocalDate.now().minusDays(1),
+                "name",
+                defaultTime.getId(),
+                2L
+        )))
+                .isInstanceOf(RoomescapeException.class)
+                .hasMessage(THEME_NOT_FOUND.getMessage());
+    }
+
+    @DisplayName("필수값이 입력되지 않은 예약 생성 요청에 대해 예외가 발생한다.")
+    @Test
+    void emptyRequiredValueTest() {
+        //todo [로빈] 이 테스트를 작성하다 보니 Response dto 에도 NotNull 검증은 필요할 것 같다는 생각이 듭니다.
+        assertAll(
+                () -> assertThatThrownBy(() -> reservationService.save(new ReservationRequest(
+                        LocalDate.now().minusDays(1),
+                        null,
+                        defaultTime.getId(),
+                        defaultTheme.getId()
+                ))).isInstanceOf(RoomescapeException.class)
+                        .hasMessage(NAME_EMPTY.getMessage())
+        );
+    }
+
+    @DisplayName("예약이 하나 존재하는 경우")
+    @Nested
+    class OneReservationExistsTest {
+
+        LocalDate defaultDate = LocalDate.now().plusDays(1);
+        Reservation defaultReservation;
+
+        @BeforeEach
+        void addDefaultReservation() {
+            defaultReservation = new Reservation("name", defaultDate, defaultTime, defaultTheme);
+            defaultReservation = reservationRepository.save(defaultReservation);
+        }
+
+        @DisplayName("이미 예약된 시간, 테마의 예약을 또 생성할 수 없다.")
+        @Test
+        void duplicatedReservationFailTest() {
+            assertThatThrownBy(() -> reservationService.save(
+                    new ReservationRequest(defaultDate, "otherName", defaultTime.getId(), defaultTheme.getId())))
+                    .isInstanceOf(RoomescapeException.class)
+                    .hasMessage(DUPLICATE_RESERVATION.getMessage());
+        }
+
+        @DisplayName("예약을 삭제할 수 있다.")
+        @Test
+        void deleteReservationTest() {
+            //when
+            reservationService.delete(1L);
+
+            //then
+            assertThat(reservationRepository.findAll()).isEmpty();
+        }
+
+        @DisplayName("존재하지 않는 예약에 대한 삭제 요청은 정상 요청으로 간주한다.")
+        @Test
+        void deleteNotExistReservationNotThrowsException() {
+            assertThatCode(() -> reservationService.delete(2L))
+                    .doesNotThrowAnyException();
+        }
+    }
+
+    @DisplayName("예약이 여러 개 존재하는 경우 모든 예약을 조회할 수 있다.")
+    @Test
+    void findAllTest() {
+        //given
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(1), defaultTime, defaultTheme));
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(2), defaultTime, defaultTheme));
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(3), defaultTime, defaultTheme));
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(4), defaultTime, defaultTheme));
+
+        //when
+        List<ReservationResponse> reservationResponses = reservationService.findAll();
+
+        //then
+        assertThat(reservationResponses).hasSize(4);
+    }
+}

From 5f25eaf54a817017f56884915941da9a17aa4e57 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 22:07:54 +0900
Subject: [PATCH 38/75] =?UTF-8?q?refactor=20:=20CollectionReservationRepos?=
 =?UTF-8?q?itory=20=EA=B0=80=20=EB=8B=A4=EB=A5=B8=20Repository=20=EB=A5=BC?=
 =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84?=
 =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/ReservationControllerTest.java |  3 +-
 .../ReservationTimeControllerTest.java        | 46 ++++++++-----------
 .../CollectionReservationRepository.java      | 35 ++------------
 .../service/ReservationServiceTest.java       |  2 +-
 .../service/ReservationTimeServiceTest.java   |  3 +-
 .../roomescape/service/ThemeServiceTest.java  |  2 +-
 6 files changed, 26 insertions(+), 65 deletions(-)

diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index ee1b61be86..5522db8150 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -21,7 +21,6 @@
 import roomescape.repository.CollectionThemeRepository;
 import roomescape.service.ReservationService;
 
-//TODO : 전체적으로 테스트 수정
 class ReservationControllerTest {
     private static final long TIME_ID = 1L;
     private static final LocalTime TIME = LocalTime.now();
@@ -35,7 +34,7 @@ class ReservationControllerTest {
     void initController() {
         CollectionReservationTimeRepository timeRepository = new CollectionReservationTimeRepository();
         CollectionThemeRepository themeRepository = new CollectionThemeRepository();
-        collectionReservationRepository = new CollectionReservationRepository(timeRepository);
+        collectionReservationRepository = new CollectionReservationRepository();
         ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository,
                 themeRepository);
         reservationController = new ReservationController(reservationService);
diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
index 7726d9e2d5..46c4f4e5bb 100644
--- a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
@@ -5,6 +5,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import roomescape.domain.ReservationTime;
@@ -15,17 +16,23 @@
 import roomescape.service.ReservationTimeService;
 
 class ReservationTimeControllerTest {
+
+    private CollectionReservationTimeRepository reservationTimeRepository;
+    private ReservationTimeController reservationTimeController;
+
+    @BeforeEach
+    void init() {
+        reservationTimeRepository = new CollectionReservationTimeRepository();
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository();
+        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
+                reservationTimeRepository);
+        reservationTimeController = new ReservationTimeController(reservationTimeService);
+    }
+
     @Test
     @DisplayName("시간을 잘 저장하는지 확인한다.")
     void save() {
-        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
-                reservationTimeRepository);
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
-                reservationTimeRepository);
-        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
         LocalTime time = LocalTime.now();
-
         ReservationTimeResponse save = reservationTimeController.save(new ReservationTimeRequest(time)).getBody();
 
         ReservationTimeResponse expected = new ReservationTimeResponse(save.id(), time);
@@ -36,12 +43,6 @@ void save() {
     @Test
     @DisplayName("시간을 잘 불러오는지 확인한다.")
     void findAll() {
-        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
-                reservationTimeRepository);
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
-                reservationTimeRepository);
-        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
         List<ReservationTimeResponse> reservationTimeResponses = reservationTimeController.findAll();
 
         Assertions.assertThat(reservationTimeResponses)
@@ -51,17 +52,13 @@ void findAll() {
     @Test
     @DisplayName("시간을 잘 지우는지 확인한다.")
     void delete() {
-        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository(
-                new ArrayList<>(List.of(new ReservationTime(1L, LocalTime.now())))
-        );
-        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
-                reservationTimeRepository);
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
-                reservationTimeRepository);
-        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
+        //given
+        reservationTimeRepository.save(new ReservationTime(1L, LocalTime.now()));
 
+        //when
         reservationTimeController.delete(1);
 
+        //then
         List<ReservationTimeResponse> reservationTimeResponses = reservationTimeController.findAll();
         Assertions.assertThat(reservationTimeResponses)
                 .isEmpty();
@@ -70,13 +67,6 @@ void delete() {
     @Test
     @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.")
     void checkRepositoryDependency() {
-        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        CollectionReservationRepository reservationRepository = new CollectionReservationRepository(
-                reservationTimeRepository);
-        ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
-                reservationTimeRepository);
-        ReservationTimeController reservationTimeController = new ReservationTimeController(reservationTimeService);
-
         boolean isRepositoryInjected = false;
 
         for (Field field : reservationTimeController.getClass().getDeclaredFields()) {
diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java
index ca6daa0aa2..6673a8f600 100644
--- a/src/test/java/roomescape/repository/CollectionReservationRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java
@@ -1,50 +1,23 @@
 package roomescape.repository;
 
-import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Predicate;
 import roomescape.domain.Reservation;
-import roomescape.domain.ReservationTime;
-import roomescape.dto.ReservationRequest;
-import roomescape.exception.RoomescapeException;
 
 public class CollectionReservationRepository implements ReservationRepository {
     private final List<Reservation> reservations;
     private final AtomicLong atomicLong;
-    private final CollectionReservationTimeRepository timeRepository;
-
-    public CollectionReservationRepository(CollectionReservationTimeRepository timeRepository) {
-        this(new ArrayList<>(), new AtomicLong(0), timeRepository);
-    }
 
-    public CollectionReservationRepository(List<Reservation> reservations, AtomicLong atomicLong,
-                                           CollectionReservationTimeRepository timeRepository) {
-        this.reservations = reservations;
-        this.atomicLong = atomicLong;
-        this.timeRepository = timeRepository;
-    }
-
-    public CollectionReservationRepository(List<Reservation> reservations,
-                                           CollectionReservationTimeRepository timeRepository) {
-        this(reservations, new AtomicLong(0), timeRepository);
-    }
 
-    private static Predicate<ReservationTime> sameId(ReservationRequest reservationRequest) {
-        return reservationTime -> reservationTime.getId() == reservationRequest.timeId();
+    public CollectionReservationRepository() {
+        this.reservations = new ArrayList<>();
+        this.atomicLong = new AtomicLong(0);
     }
 
     @Override
     public Reservation save(Reservation reservation) {
-        //TODO 안해도 되는지 확인하기
-        ReservationTime findTime = timeRepository.findAll().stream()
-                .filter(reservationTime -> reservationTime.getId() == reservation.getReservationTime().getId())
-                .findFirst()
-                .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
-        Reservation saved = new Reservation(atomicLong.incrementAndGet(), reservation.getName(), reservation.getDate(),
-                findTime, reservation.getTheme());
+        Reservation saved = new Reservation(atomicLong.incrementAndGet(), reservation);
         reservations.add(saved);
         return saved;
     }
diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java
index f49356cff9..1bf6505187 100644
--- a/src/test/java/roomescape/service/ReservationServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationServiceTest.java
@@ -41,7 +41,7 @@ class ReservationServiceTest {
     void initService() {
         CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
         ThemeRepository themeRepository = new CollectionThemeRepository();
-        reservationRepository = new CollectionReservationRepository(reservationTimeRepository);
+        reservationRepository = new CollectionReservationRepository();
         reservationService = new ReservationService(reservationRepository, reservationTimeRepository, themeRepository);
 
         defaultTime = reservationTimeRepository.save(defaultTime);
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index 3865031b59..2d4c478b70 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -19,8 +19,7 @@ class ReservationTimeServiceTest {
     @BeforeEach
     void initService() {
         CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        CollectionReservationRepository reservationRepository =
-                new CollectionReservationRepository(reservationTimeRepository);
+        CollectionReservationRepository reservationRepository = new CollectionReservationRepository();
         reservationTimeService = new ReservationTimeService(reservationRepository, reservationTimeRepository);
     }
 
diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java
index 45d32c1856..04220a1b6e 100644
--- a/src/test/java/roomescape/service/ThemeServiceTest.java
+++ b/src/test/java/roomescape/service/ThemeServiceTest.java
@@ -36,7 +36,7 @@ class ThemeServiceTest {
     void initService() {
         themeRepository = new CollectionThemeRepository();
         reservationTimeRepository = new CollectionReservationTimeRepository();
-        reservationRepository = new CollectionReservationRepository(reservationTimeRepository);
+        reservationRepository = new CollectionReservationRepository();
         themeService = new ThemeService(themeRepository, reservationRepository);
     }
 

From b4b52b4e75b909173c010700533177aad43affb4 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 22:10:26 +0900
Subject: [PATCH 39/75] =?UTF-8?q?refactor=20:=20getter=20=EB=8C=80?=
 =?UTF-8?q?=EC=8B=A0=20=EA=B5=AC=ED=98=84=EB=90=9C=20=EB=8F=84=EB=A9=94?=
 =?UTF-8?q?=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20=EC=82=AC?=
 =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/CollectionReservationTimeRepository.java         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index fc8901ecf4..cac0956a7b 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -40,7 +40,7 @@ public boolean existsByStartAt(LocalTime startAt) {
     @Override
     public Optional<ReservationTime> findById(long id) {
         return reservationTimes.stream()
-                .filter(reservationTime -> reservationTime.getId() == id) //Todo 메서드로 바꾸기
+                .filter(reservationTime -> reservationTime.isIdOf(id)) //Todo 메서드로 바꾸기
                 .findFirst();
     }
 

From cc62f4b565f06565c0a496dd5d212c4e03daf851 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Wed, 1 May 2024 22:10:35 +0900
Subject: [PATCH 40/75] =?UTF-8?q?refactor=20:=20=ED=95=B4=EA=B2=B0?=
 =?UTF-8?q?=EB=90=9C=20TODO=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/roomescape/repository/ReservationTimeRepository.java    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java
index f30eeb9c60..ca04e207b9 100644
--- a/src/main/java/roomescape/repository/ReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java
@@ -8,7 +8,6 @@
 public interface ReservationTimeRepository {
     ReservationTime save(ReservationTime reservationTime);
 
-    //Todo 메서드명 고민
     boolean existsByStartAt(LocalTime startAt);
 
     Optional<ReservationTime> findById(long id);

From f69070806acacb42fb32fcff068fe3804c222837 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 09:46:37 +0900
Subject: [PATCH 41/75] =?UTF-8?q?style:=20=EB=88=84=EB=9D=BD=EB=90=9C=20st?=
 =?UTF-8?q?atic=20import=20=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/roomescape/service/ReservationService.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index de04fb3a12..ba706b53cd 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,5 +1,6 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.*;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION;
 import static roomescape.exception.ExceptionType.PAST_TIME;
 import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
@@ -38,7 +39,7 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         ReservationTime requestedTime = reservationTimeRepository.findById(reservationRequest.timeId())
                 .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
         Theme requestedTheme = themeRepository.findById(reservationRequest.themeId())
-                .orElseThrow(() -> new RoomescapeException(ExceptionType.THEME_NOT_FOUND));
+                .orElseThrow(() -> new RoomescapeException(THEME_NOT_FOUND));
 
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),

From 1e96e33f00836371f67cb9d516621d451bb5aa74 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 09:48:14 +0900
Subject: [PATCH 42/75] =?UTF-8?q?style:=20=EC=B2=98=EB=A6=AC=EB=90=9C=20to?=
 =?UTF-8?q?do=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/CollectionReservationTimeRepository.java         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index cac0956a7b..cc074089a3 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -40,7 +40,7 @@ public boolean existsByStartAt(LocalTime startAt) {
     @Override
     public Optional<ReservationTime> findById(long id) {
         return reservationTimes.stream()
-                .filter(reservationTime -> reservationTime.isIdOf(id)) //Todo 메서드로 바꾸기
+                .filter(reservationTime -> reservationTime.isIdOf(id))
                 .findFirst();
     }
 

From 06bb27c1f465046b99161869cd6d4f43497946b5 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 10:32:36 +0900
Subject: [PATCH 43/75] =?UTF-8?q?test:=20ReservationTimeService=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/ReservationTimeServiceTest.java   | 95 +++++++++++++++++--
 1 file changed, 85 insertions(+), 10 deletions(-)

diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index 2d4c478b70..bc70c5d82f 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -1,36 +1,111 @@
 package roomescape.service;
 
+import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME;
+import static roomescape.exception.ExceptionType.INVALID_DELETE_TIME;
 
+import java.time.LocalDate;
 import java.time.LocalTime;
+import java.util.List;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 import roomescape.dto.ReservationTimeRequest;
+import roomescape.dto.ReservationTimeResponse;
 import roomescape.exception.RoomescapeException;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.CollectionThemeRepository;
 
 class ReservationTimeServiceTest {
 
+    private CollectionReservationRepository reservationRepository;
+    private CollectionReservationTimeRepository reservationTimeRepository;
     private ReservationTimeService reservationTimeService;
 
     @BeforeEach
     void initService() {
-        CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository();
-        CollectionReservationRepository reservationRepository = new CollectionReservationRepository();
+        reservationRepository = new CollectionReservationRepository();
+        reservationTimeRepository = new CollectionReservationTimeRepository();
         reservationTimeService = new ReservationTimeService(reservationRepository, reservationTimeRepository);
     }
 
+    @DisplayName("예약 시간이 하나 존재할 때")
+    @Nested
+    class OneReservationTimeExists {
+        private static final LocalTime SAVED_TIME = LocalTime.of(10, 0);
+
+        @BeforeEach
+        void addDefaultTime() {
+            ReservationTimeRequest reservationTimeRequest = new ReservationTimeRequest(SAVED_TIME);
+            reservationTimeService.save(reservationTimeRequest);
+        }
+
+        @DisplayName("정상적으로 시간을 생성할 수 있다.")
+        @Test
+        void saveReservationTimeTest() {
+            assertThatCode(() ->
+                    reservationTimeService.save(new ReservationTimeRequest(SAVED_TIME.plusHours(1))))
+                    .doesNotThrowAnyException();
+        }
+
+        @Test
+        @DisplayName("중복된 시간은 생성할 수 없는지 검증")
+        void saveFailCauseDuplicate() {
+            assertThatThrownBy(() -> reservationTimeService.save(new ReservationTimeRequest(SAVED_TIME)))
+                    .isInstanceOf(RoomescapeException.class)
+                    .hasMessage(DUPLICATE_RESERVATION_TIME.getMessage());
+        }
+
+        @DisplayName("저장된 시간을 삭제할 수 있다.")
+        @Test
+        void deleteByIdTest() {
+            //when
+            reservationTimeService.delete(1L);
+
+            //then
+            assertThat(reservationTimeRepository.findAll())
+                    .isEmpty();
+        }
+
+        @DisplayName("예약 시간을 사용하는 예약이 있으면 예약을 삭제할 수 없다.")
+        @Test
+        void usedReservationTimeDeleteTest() {
+            //given
+            reservationRepository.save(new Reservation(
+                    "name",
+                    LocalDate.now(),
+                    new ReservationTime(1L, SAVED_TIME),
+                    new Theme(1L, "name", "description", "thumbnail")
+            ));
+
+            //when & then
+            assertThatCode(() -> reservationTimeService.delete(1L))
+                    .isInstanceOf(RoomescapeException.class)
+                    .hasMessage(INVALID_DELETE_TIME.getMessage());
+        }
+    }
+
+    @DisplayName("저장된 시간을 모두 조회할 수 있다.")
     @Test
-    @DisplayName("중복된 시간은 생성할 수 없는지 검증")
-    void saveFailCauseDuplicate() {
-        ReservationTimeRequest reservationTimeRequest = new ReservationTimeRequest(LocalTime.of(10, 0));
-        reservationTimeService.save(reservationTimeRequest);
-
-        Assertions.assertThatThrownBy(() -> reservationTimeService.save(reservationTimeRequest))
-                .isInstanceOf(RoomescapeException.class)
-                .hasMessage(DUPLICATE_RESERVATION_TIME.getMessage());
+    void findAllTest() {
+        //given
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(10, 0)));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(11, 0)));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 0)));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(13, 0)));
+
+        //when
+        List<ReservationTimeResponse> reservationTimeResponses = reservationTimeService.findAll();
+
+        //then
+        assertThat(reservationTimeResponses)
+                .hasSize(4);
     }
 }

From 225539175a7c0dd3b43f6f2a5157f92eeea88e8e Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 10:43:38 +0900
Subject: [PATCH 44/75] =?UTF-8?q?test:=20AvailableTimeService=20=EB=A1=9C?=
 =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/AvailableTimeServiceTest.java     | 59 +++++++++++++++++++
 1 file changed, 59 insertions(+)
 create mode 100644 src/test/java/roomescape/service/AvailableTimeServiceTest.java

diff --git a/src/test/java/roomescape/service/AvailableTimeServiceTest.java b/src/test/java/roomescape/service/AvailableTimeServiceTest.java
new file mode 100644
index 0000000000..87b5427efd
--- /dev/null
+++ b/src/test/java/roomescape/service/AvailableTimeServiceTest.java
@@ -0,0 +1,59 @@
+package roomescape.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
+import roomescape.dto.AvailableTimeResponse;
+import roomescape.repository.CollectionReservationRepository;
+import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.ReservationRepository;
+import roomescape.repository.ReservationTimeRepository;
+
+class AvailableTimeServiceTest {
+
+    private AvailableTimeService availableTimeService;
+    private ReservationRepository reservationRepository;
+    private ReservationTimeRepository reservationTimeRepository;
+
+    @BeforeEach
+    void init() {
+        reservationRepository = new CollectionReservationRepository();
+        reservationTimeRepository = new CollectionReservationTimeRepository();
+        availableTimeService = new AvailableTimeService(reservationRepository, reservationTimeRepository);
+    }
+
+    @DisplayName("날짜와 테마, 시간에 대한 예약 내역을 확인할 수 있다.")
+    @Test
+    void findAvailableTimeTest() {
+        //given
+        Theme DEFUALT_THEME = new Theme(1L, "name", "description", "thumbnail");
+        ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(11, 0)));
+        ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 0)));
+        ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(13, 0)));
+        ReservationTime reservationTime4 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(14, 0)));
+
+        LocalDate selectedDate = LocalDate.of(2024, 1, 1);
+        reservationRepository.save(new Reservation("name", selectedDate, reservationTime1, DEFUALT_THEME));
+        reservationRepository.save(new Reservation("name", selectedDate, reservationTime3, DEFUALT_THEME));
+
+        //when
+        List<AvailableTimeResponse> availableTimeResponses = availableTimeService.findByThemeAndDate(selectedDate,
+                DEFUALT_THEME.getId());
+
+        //then
+        assertThat(availableTimeResponses).containsExactlyInAnyOrder(
+                new AvailableTimeResponse(1L, reservationTime1.getStartAt(), true),
+                new AvailableTimeResponse(2L, reservationTime2.getStartAt(), false),
+                new AvailableTimeResponse(3L, reservationTime3.getStartAt(), true),
+                new AvailableTimeResponse(4L, reservationTime4.getStartAt(), false)
+        );
+    }
+}

From ee5b1150c753efb78fe0dd033abc22aad49a7547 Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 10:45:05 +0900
Subject: [PATCH 45/75] =?UTF-8?q?style:=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?=
 =?UTF-8?q?=EC=A0=81=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/roomescape/service/ReservationService.java    | 3 +--
 src/test/java/roomescape/RoomescapeApplicationTest.java     | 6 +++---
 .../controller/ReservationTimeControllerTest.java           | 1 -
 .../java/roomescape/service/ReservationTimeServiceTest.java | 5 ++---
 4 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index ba706b53cd..a396df0645 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,9 +1,9 @@
 package roomescape.service;
 
-import static roomescape.exception.ExceptionType.*;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION;
 import static roomescape.exception.ExceptionType.PAST_TIME;
 import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
+import static roomescape.exception.ExceptionType.THEME_NOT_FOUND;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -15,7 +15,6 @@
 import roomescape.dto.ReservationResponse;
 import roomescape.dto.ReservationTimeResponse;
 import roomescape.dto.ThemeResponse;
-import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ReservationTimeRepository;
diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java
index 326a3ff677..6c9ad1d0c7 100644
--- a/src/test/java/roomescape/RoomescapeApplicationTest.java
+++ b/src/test/java/roomescape/RoomescapeApplicationTest.java
@@ -6,8 +6,8 @@
 @SpringBootTest
 class RoomescapeApplicationTest {
 
-        @Test
-        void contextLoads() {
-        }
+    @Test
+    void contextLoads() {
+    }
 
 }
diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
index 46c4f4e5bb..c6082b30a5 100644
--- a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
@@ -2,7 +2,6 @@
 
 import java.lang.reflect.Field;
 import java.time.LocalTime;
-import java.util.ArrayList;
 import java.util.List;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index bc70c5d82f..cac7188ab2 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -1,14 +1,14 @@
 package roomescape.service;
 
-import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME;
 import static roomescape.exception.ExceptionType.INVALID_DELETE_TIME;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
-import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -21,7 +21,6 @@
 import roomescape.exception.RoomescapeException;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
-import roomescape.repository.CollectionThemeRepository;
 
 class ReservationTimeServiceTest {
 

From 77a3e2432825e1ab5246ea5bb119af38bb493d1f Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 11:41:23 +0900
Subject: [PATCH 46/75] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?=EC=A0=84=EC=97=90=20theme=20=ED=85=8C=EC=9D=B4=EB=B8=94?=
 =?UTF-8?q?=EB=8F=84=20=EC=B4=88=EA=B8=B0=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/JdbcTemplateReservationRepositoryTest.java  | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
index 400186c4b1..79dfd24631 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
@@ -28,9 +28,16 @@ class JdbcTemplateReservationRepositoryTest {
     void init() {
         jdbcTemplate.update("delete from reservation");
         jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
+
         jdbcTemplate.update("delete from reservation_time");
         jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
         jdbcTemplate.update("insert into reservation_time(start_at) values('11:56')");
+
+        jdbcTemplate.update("delete from theme");
+        jdbcTemplate.update("ALTER TABLE theme alter column id restart with 1");
+        jdbcTemplate.update(
+                "insert into theme (name, description, thumbnail) values('name', 'description', 'thumbnail')");
+
     }
 
     @Test

From 3689dc743073aa6c913afd3a29ae65b7144c108d Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 11:47:30 +0900
Subject: [PATCH 47/75] =?UTF-8?q?docs:=20=EC=9D=B8=EA=B8=B0=20=ED=85=8C?=
 =?UTF-8?q?=EB=A7=88=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index 7c7b3324bd..41fc748304 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@
     - [x] 예약이 있는 테마를 삭제 요청시 에러
 
 - [ ] 사용자 예약 기능 추가
+- [ ] 인기 테마 기능 추가
 
 # API 명세
 

From 0516901892ec253e933d2ae0763291991e7f86ee Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 11:47:47 +0900
Subject: [PATCH 48/75] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B8=B0=20=ED=85=8C?=
 =?UTF-8?q?=EB=A7=88=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                                     |  2 +-
 .../controller/ThemeController.java           |  9 +++
 .../roomescape/controller/UserController.java |  5 ++
 .../JdbcTemplateThemeRepository.java          | 32 +++++----
 .../repository/ThemeRepository.java           |  3 +
 .../java/roomescape/service/ThemeService.java |  7 ++
 src/main/resources/static/js/ranking.js       | 72 +++++++++++--------
 .../repository/CollectionThemeRepository.java |  6 ++
 8 files changed, 94 insertions(+), 42 deletions(-)

diff --git a/README.md b/README.md
index 41fc748304..fd820a31e5 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
     - [x] 예약이 있는 테마를 삭제 요청시 에러
 
 - [ ] 사용자 예약 기능 추가
-- [ ] 인기 테마 기능 추가
+- [x] 인기 테마 기능 추가
 
 # API 명세
 
diff --git a/src/main/java/roomescape/controller/ThemeController.java b/src/main/java/roomescape/controller/ThemeController.java
index bc38a24576..dbb0cb2520 100644
--- a/src/main/java/roomescape/controller/ThemeController.java
+++ b/src/main/java/roomescape/controller/ThemeController.java
@@ -1,6 +1,7 @@
 package roomescape.controller;
 
 import java.net.URI;
+import java.time.LocalDate;
 import java.util.List;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -9,6 +10,7 @@
 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.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import roomescape.dto.ThemeRequest;
 import roomescape.dto.ThemeResponse;
@@ -29,6 +31,13 @@ public List<ThemeResponse> findAll() {
         return themeService.findAll();
     }
 
+    @GetMapping("/ranking")
+    public List<ThemeResponse> findAndOrderByPopularity(@RequestParam LocalDate start,
+                                                        @RequestParam LocalDate end,
+                                                        @RequestParam int count) {
+        return themeService.findAndOrderByPopularity(start, end, count);
+    }
+
     @PostMapping
     public ResponseEntity<ThemeResponse> save(@RequestBody ThemeRequest themeRequest) {
         ThemeResponse saved = themeService.save(themeRequest);
diff --git a/src/main/java/roomescape/controller/UserController.java b/src/main/java/roomescape/controller/UserController.java
index 39d95139d3..86ecc0f8ce 100644
--- a/src/main/java/roomescape/controller/UserController.java
+++ b/src/main/java/roomescape/controller/UserController.java
@@ -9,4 +9,9 @@ public class UserController {
     public String reservationPage() {
         return "reservation";
     }
+
+    @GetMapping("/")
+    public String bestThemePage() {
+        return "index";
+    }
 }
diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
index 7d44bd86be..497b82c526 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
@@ -1,9 +1,11 @@
 package roomescape.repository;
 
 import java.sql.PreparedStatement;
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Optional;
 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;
@@ -12,6 +14,14 @@
 @Repository
 public class JdbcTemplateThemeRepository implements ThemeRepository {
     private final JdbcTemplate jdbcTemplate;
+    private RowMapper<Theme> themeRowMapper = (rs, rowNum) -> {
+        long id = rs.getLong("id");
+        String name = rs.getString("name");
+        String description = rs.getString("description");
+        String thumbnail = rs.getString("thumbnail");
+        return new Theme(id, name, description, thumbnail);
+    };
+    ;
 
     public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
@@ -19,24 +29,20 @@ public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate) {
 
     @Override
     public List<Theme> findAll() {
-        return jdbcTemplate.query("select ID, NAME, DESCRIPTION, THUMBNAIL from THEME", (rs, rowNum) -> {
-            long id = rs.getLong(1);
-            String name = rs.getString(2);
-            String description = rs.getString(3);
-            String thumbnail = rs.getString(4);
-            return new Theme(id, name, description, thumbnail);
-        });
+        return jdbcTemplate.query("select ID, NAME, DESCRIPTION, THUMBNAIL from THEME", themeRowMapper);
+    }
+
+    @Override
+    public List<Theme> findAndOrderByPopularity(LocalDate start, LocalDate end, int count) {
+        return jdbcTemplate.query(
+                "select th.*, count(*) as count from theme th join reservation r on r.theme_id = th.id where PARSEDATETIME(r.date,'yyyy-MM-dd') >= PARSEDATETIME(?,'yyyy-MM-dd') and PARSEDATETIME(r.date,'yyyy-MM-dd') <= PARSEDATETIME(?,'yyyy-MM-dd') group by th.id order by count desc limit ?",
+                themeRowMapper, start, end, count);
     }
 
     @Override
     public Optional<Theme> findById(long id) {
         List<Theme> themes = jdbcTemplate.query("select id, name, description, thumbnail from theme where id = ?",
-                (rs, rowNum) -> {
-                    String name = rs.getString("name");
-                    String description = rs.getString("description");
-                    String thumbnail = rs.getString("thumbnail");
-                    return new Theme(id, name, description, thumbnail);
-                }, id);
+                themeRowMapper, id);
         return themes.stream().findFirst();
     }
 
diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java
index a43cb24851..f9cdad4015 100644
--- a/src/main/java/roomescape/repository/ThemeRepository.java
+++ b/src/main/java/roomescape/repository/ThemeRepository.java
@@ -1,5 +1,6 @@
 package roomescape.repository;
 
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Optional;
 import roomescape.domain.Theme;
@@ -7,6 +8,8 @@
 public interface ThemeRepository {
     List<Theme> findAll();
 
+    List<Theme> findAndOrderByPopularity(LocalDate start, LocalDate end, int count);
+
     Optional<Theme> findById(long id);
 
     Theme save(Theme theme);
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index 3fd8f6bab1..f417befdc9 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -1,5 +1,6 @@
 package roomescape.service;
 
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Objects;
 import org.springframework.stereotype.Service;
@@ -44,6 +45,12 @@ public List<ThemeResponse> findAll() {
                 .toList();
     }
 
+    public List<ThemeResponse> findAndOrderByPopularity(LocalDate start, LocalDate end, int count) {
+        return themeRepository.findAndOrderByPopularity(start, end, count).stream()
+                .map(this::toResponse)
+                .toList();
+    }
+
     public void delete(long id) {
         //todo : 변수명 고민
         boolean invalidDelete = reservationRepository.findAll().stream()
diff --git a/src/main/resources/static/js/ranking.js b/src/main/resources/static/js/ranking.js
index dee05edf0b..c4b20948b1 100644
--- a/src/main/resources/static/js/ranking.js
+++ b/src/main/resources/static/js/ranking.js
@@ -1,25 +1,41 @@
 document.addEventListener('DOMContentLoaded', () => {
-    /*
-    TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출
-    */
-    requestRead('/') // 인기 테마 목록 조회 API endpoint
-        .then(render)
-        .catch(error => console.error('Error fetching times:', error));
+  /*
+  TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출
+  */
+  const today = new Date();
+  let startDate = formatDate(minusDay(today, 7));
+  let endDate = formatDate(minusDay(today, 1));
+  const count = 10;
+  const endpoint = `/themes/ranking?start=${startDate}&end=${endDate}&count=${count}`;
+  requestRead(endpoint) // 인기 테마 목록 조회 API endpoint
+      .then(render)
+      .catch(error => console.error('Error fetching times:', error));
 });
 
+function minusDay(date, minusValue) {
+  return new Date(new Date(date).setDate(date.getDate() - minusValue));
+}
+
+function formatDate(date) {
+  const year = date.getFullYear();
+  const month = ('0' + (date.getMonth() + 1)).slice(-2);
+  const day = ('0' + date.getDate()).slice(-2);
+  return year + '-' + month + '-' + day;
+}
+
 function render(data) {
-    const container = document.getElementById('theme-ranking');
-
-    /*
-    TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출 후 렌더링
-          response 명세에 맞춰 name, thumbnail, description 값 설정
-    */
-    data.forEach(theme => {
-        const name = '';
-        const thumbnail = '';
-        const description = '';
-
-        const htmlContent = `
+  const container = document.getElementById('theme-ranking');
+
+  /*
+  TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출 후 렌더링
+        response 명세에 맞춰 name, thumbnail, description 값 설정
+  */
+  data.forEach(theme => {
+    const name = theme.name;
+    const thumbnail = theme.thumbnail;
+    const description = theme.description;
+
+    const htmlContent = `
             <img class="mr-3 img-thumbnail" src="${thumbnail}" alt="${name}">
             <div class="media-body">
                 <h5 class="mt-0 mb-1">${name}</h5>
@@ -27,18 +43,18 @@ function render(data) {
             </div>
         `;
 
-        const div = document.createElement('li');
-        div.className = 'media my-4';
-        div.innerHTML = htmlContent;
+    const div = document.createElement('li');
+    div.className = 'media my-4';
+    div.innerHTML = htmlContent;
 
-        container.appendChild(div);
-    })
+    container.appendChild(div);
+  })
 }
 
 function requestRead(endpoint) {
-    return fetch(endpoint)
-        .then(response => {
-            if (response.status === 200) return response.json();
-            throw new Error('Read failed');
-        });
+  return fetch(endpoint)
+      .then(response => {
+        if (response.status === 200) return response.json();
+        throw new Error('Read failed');
+      });
 }
diff --git a/src/test/java/roomescape/repository/CollectionThemeRepository.java b/src/test/java/roomescape/repository/CollectionThemeRepository.java
index a851648ec3..5df15fb0c1 100644
--- a/src/test/java/roomescape/repository/CollectionThemeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionThemeRepository.java
@@ -1,5 +1,6 @@
 package roomescape.repository;
 
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Optional;
 import roomescape.domain.Theme;
@@ -10,6 +11,11 @@ public List<Theme> findAll() {
         return null;
     }
 
+    @Override
+    public List<Theme> findAndOrderByPopularity(LocalDate start, LocalDate end, int count) {
+        return null;
+    }
+
     @Override
     public Optional<Theme> findById(long id) {
         return Optional.of(new Theme(id, "이름", "설명", "썸네일"));

From 2789daefd09acbdcf85c0aa568850f3de0279b9f Mon Sep 17 00:00:00 2001
From: zangsu <zangsu_@naver.com>
Date: Thu, 2 May 2024 15:27:43 +0900
Subject: [PATCH 49/75] =?UTF-8?q?refactor:=20ExceptionType=20=EB=84=A4?=
 =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/roomescape/domain/Reservation.java   | 16 +++++------
 .../roomescape/domain/ReservationTime.java    |  4 +--
 src/main/java/roomescape/domain/Theme.java    | 11 +++++---
 .../roomescape/exception/ExceptionType.java   | 27 +++++++++----------
 .../service/ReservationService.java           | 12 ++++-----
 .../service/ReservationTimeService.java       |  4 +--
 .../java/roomescape/service/ThemeService.java |  8 +++---
 .../roomescape/domain/ReservationTest.java    | 16 +++++------
 .../domain/ReservationTimeTest.java           |  4 +--
 .../java/roomescape/domain/ThemeTest.java     | 12 ++++-----
 .../service/ReservationServiceTest.java       | 16 +++++------
 .../service/ReservationTimeServiceTest.java   |  4 +--
 .../roomescape/service/ThemeServiceTest.java  |  4 +--
 13 files changed, 70 insertions(+), 68 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index 6ac260905d..9e0abaaf20 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -1,9 +1,9 @@
 package roomescape.domain;
 
-import static roomescape.exception.ExceptionType.DATE_EMPTY;
-import static roomescape.exception.ExceptionType.NAME_EMPTY;
-import static roomescape.exception.ExceptionType.THEME_EMPTY;
-import static roomescape.exception.ExceptionType.TIME_EMPTY;
+import static roomescape.exception.ExceptionType.EMPTY_DATE;
+import static roomescape.exception.ExceptionType.EMPTY_NAME;
+import static roomescape.exception.ExceptionType.EMPTY_THEME;
+import static roomescape.exception.ExceptionType.EMPTY_TIME;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -36,25 +36,25 @@ public Reservation(Long id, String name, LocalDate date, ReservationTime time, T
 
     private void validateTheme(Theme theme) {
         if (theme == null) {
-            throw new RoomescapeException(THEME_EMPTY);
+            throw new RoomescapeException(EMPTY_THEME);
         }
     }
 
     private void validateTime(ReservationTime time) {
         if (time == null) {
-            throw new RoomescapeException(TIME_EMPTY);
+            throw new RoomescapeException(EMPTY_TIME);
         }
     }
 
     private void validateDate(LocalDate date) {
         if (date == null) {
-            throw new RoomescapeException(DATE_EMPTY);
+            throw new RoomescapeException(EMPTY_DATE);
         }
     }
 
     private void validateName(String name) {
         if (name == null || name.isBlank()) {
-            throw new RoomescapeException(NAME_EMPTY);
+            throw new RoomescapeException(EMPTY_NAME);
         }
     }
 
diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index bfd1bd8763..50f22a934e 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -1,6 +1,6 @@
 package roomescape.domain;
 
-import static roomescape.exception.ExceptionType.TIME_EMPTY;
+import static roomescape.exception.ExceptionType.EMPTY_TIME;
 
 import java.time.LocalTime;
 import java.util.Objects;
@@ -16,7 +16,7 @@ public ReservationTime(LocalTime startAt) {
 
     public ReservationTime(Long id, LocalTime startAt) {
         if (startAt == null) {
-            throw new RoomescapeException(TIME_EMPTY);
+            throw new RoomescapeException(EMPTY_TIME);
         }
         this.id = id;
         this.startAt = startAt;
diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index cc7ada5a3a..82483a5a3f 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -1,7 +1,10 @@
 package roomescape.domain;
 
+import static roomescape.exception.ExceptionType.EMPTY_DESCRIPTION;
+import static roomescape.exception.ExceptionType.EMPTY_NAME;
+import static roomescape.exception.ExceptionType.EMPTY_THUMBNAIL;
+
 import java.util.Objects;
-import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 
 public class Theme {
@@ -27,19 +30,19 @@ public Theme(Long id, String name, String description, String thumbnail) {
     private void validateName(String name) {
         if (name == null || name.isBlank()) {
             //TODO : NAME_EMPYT 재사용 고민
-            throw new RoomescapeException(ExceptionType.NAME_EMPTY);
+            throw new RoomescapeException(EMPTY_NAME);
         }
     }
 
     private void validateDescription(String description) {
         if (description == null || description.isBlank()) {
-            throw new RoomescapeException(ExceptionType.DESCRIPTION_EMPTY);
+            throw new RoomescapeException(EMPTY_DESCRIPTION);
         }
     }
 
     private void validateThumbnail(String thumbnail) {
         if (thumbnail == null || thumbnail.isBlank()) {
-            throw new RoomescapeException(ExceptionType.THUMBNAIL_EMPTY);
+            throw new RoomescapeException(EMPTY_THUMBNAIL);
         }
     }
 
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index 7725238da8..2ef6842495 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -4,26 +4,23 @@
 
 import org.springframework.http.HttpStatus;
 
-//Todo 상태코드 고민해보기
+//Todo 상태코드 고민해보기 -> 4~6 단계 인증 인가가 들어갈 때를 대비
 public enum ExceptionType {
-
-    NAME_EMPTY(BAD_REQUEST, "이름은 필수 값입니다."),
-    TIME_EMPTY(BAD_REQUEST, "시작 시간은 필수 값입니다."),
-    DATE_EMPTY(BAD_REQUEST, "날짜는 필수값 입니다."),
-    THEME_EMPTY(BAD_REQUEST, "테마는 필수값 입니다."),
-    DESCRIPTION_EMPTY(BAD_REQUEST, "테마 설명은 필수값 입니다."),
-    THUMBNAIL_EMPTY(BAD_REQUEST, "테마 썸네일은 필수값 입니다."),
-    PAST_TIME(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
+    EMPTY_NAME(BAD_REQUEST, "이름은 필수 값입니다."),
+    EMPTY_TIME(BAD_REQUEST, "시작 시간은 필수 값입니다."),
+    EMPTY_DATE(BAD_REQUEST, "날짜는 필수값 입니다."),
+    EMPTY_THEME(BAD_REQUEST, "테마는 필수값 입니다."),
+    EMPTY_DESCRIPTION(BAD_REQUEST, "테마 설명은 필수값 입니다."),
+    EMPTY_THUMBNAIL(BAD_REQUEST, "테마 썸네일은 필수값 입니다."),
+    PAST_TIME_RESERVATION(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
     DUPLICATE_RESERVATION(BAD_REQUEST, "같은 시간에 이미 예약이 존재합니다."),
     DUPLICATE_RESERVATION_TIME(BAD_REQUEST, "이미 예약시간이 존재합니다."),
     DUPLICATE_THEME(BAD_REQUEST, "이미 동일한 테마가 존재합니다."),
     INVALID_DATE_TIME_FORMAT(BAD_REQUEST, "해석할 수 없는 날짜, 시간 포맷입니다."),
-    //Todo 이름 변경,
-    INVALID_DELETE_TIME(BAD_REQUEST, "예약이 존재하는 시간은 삭제할 수 없습니다."),
-    INVALID_DELETE_THEME(BAD_REQUEST, "예약이 존재하는 테마는 삭제할 수 없습니다."),
-    RESERVATION_TIME_NOT_FOUND(BAD_REQUEST, "존재하지 않는 시간입니다."),
-    //todo 이름 변경,
-    THEME_NOT_FOUND(BAD_REQUEST, "없는 테마입니다."),
+    DELETE_USED_TIME(BAD_REQUEST, "예약이 존재하는 시간은 삭제할 수 없습니다."),
+    DELETE_USED_THEME(BAD_REQUEST, "예약이 존재하는 테마는 삭제할 수 없습니다."),
+    NOT_FOUND_RESERVATION_TIME(BAD_REQUEST, "존재하지 않는 시간입니다."),
+    NOT_FOUND_THEME(BAD_REQUEST, "없는 테마입니다."),
     ;
 
     private final HttpStatus status;
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index a396df0645..12c392135b 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -1,9 +1,9 @@
 package roomescape.service;
 
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION;
-import static roomescape.exception.ExceptionType.PAST_TIME;
-import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
-import static roomescape.exception.ExceptionType.THEME_NOT_FOUND;
+import static roomescape.exception.ExceptionType.NOT_FOUND_RESERVATION_TIME;
+import static roomescape.exception.ExceptionType.NOT_FOUND_THEME;
+import static roomescape.exception.ExceptionType.PAST_TIME_RESERVATION;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -36,9 +36,9 @@ public ReservationService(ReservationRepository reservationRepository,
     public ReservationResponse save(ReservationRequest reservationRequest) {
 
         ReservationTime requestedTime = reservationTimeRepository.findById(reservationRequest.timeId())
-                .orElseThrow(() -> new RoomescapeException(RESERVATION_TIME_NOT_FOUND));
+                .orElseThrow(() -> new RoomescapeException(NOT_FOUND_RESERVATION_TIME));
         Theme requestedTheme = themeRepository.findById(reservationRequest.themeId())
-                .orElseThrow(() -> new RoomescapeException(THEME_NOT_FOUND));
+                .orElseThrow(() -> new RoomescapeException(NOT_FOUND_THEME));
 
         Reservation beforeSave = new Reservation(
                 reservationRequest.name(),
@@ -54,7 +54,7 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
         }
 
         if (isBefore(beforeSave)) {
-            throw new RoomescapeException(PAST_TIME);
+            throw new RoomescapeException(PAST_TIME_RESERVATION);
         }
 
         Reservation saved = reservationRepository.save(beforeSave);
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 74db95e3a9..31584562a7 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -1,7 +1,7 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.DELETE_USED_TIME;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME;
-import static roomescape.exception.ExceptionType.INVALID_DELETE_TIME;
 
 import java.util.List;
 import org.springframework.stereotype.Service;
@@ -51,7 +51,7 @@ public void delete(long id) {
         boolean invalidDelete = reservations.stream()
                 .anyMatch(reservation -> reservation.isReservationTimeOf(id));
         if (invalidDelete) {
-            throw new RoomescapeException(INVALID_DELETE_TIME);
+            throw new RoomescapeException(DELETE_USED_TIME);
         }
         reservationTimeRepository.delete(id);
     }
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index d06634420a..9f355f8b71 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -1,12 +1,14 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.DELETE_USED_THEME;
+import static roomescape.exception.ExceptionType.DUPLICATE_THEME;
+
 import java.time.LocalDate;
 import java.util.List;
 import org.springframework.stereotype.Service;
 import roomescape.domain.Theme;
 import roomescape.dto.ThemeRequest;
 import roomescape.dto.ThemeResponse;
-import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ThemeRepository;
@@ -26,7 +28,7 @@ public ThemeResponse save(ThemeRequest themeRequest) {
         boolean hasDuplicateTheme = themeRepository.findAll().stream()
                 .anyMatch(theme -> theme.isNameOf(themeRequest.name()));
         if (hasDuplicateTheme) {
-            throw new RoomescapeException(ExceptionType.DUPLICATE_THEME);
+            throw new RoomescapeException(DUPLICATE_THEME);
         }
         Theme saved = themeRepository.save(
                 new Theme(themeRequest.name(), themeRequest.description(), themeRequest.thumbnail()));
@@ -54,7 +56,7 @@ public void delete(long id) {
         boolean invalidDelete = reservationRepository.findAll().stream()
                 .anyMatch(reservation -> reservation.isThemeOf(id));
         if (invalidDelete) {
-            throw new RoomescapeException(ExceptionType.INVALID_DELETE_THEME);
+            throw new RoomescapeException(DELETE_USED_THEME);
         }
         themeRepository.delete(id);
     }
diff --git a/src/test/java/roomescape/domain/ReservationTest.java b/src/test/java/roomescape/domain/ReservationTest.java
index 17b021a41d..6c20ebe0a0 100644
--- a/src/test/java/roomescape/domain/ReservationTest.java
+++ b/src/test/java/roomescape/domain/ReservationTest.java
@@ -2,10 +2,10 @@
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertAll;
-import static roomescape.exception.ExceptionType.DATE_EMPTY;
-import static roomescape.exception.ExceptionType.NAME_EMPTY;
-import static roomescape.exception.ExceptionType.THEME_EMPTY;
-import static roomescape.exception.ExceptionType.TIME_EMPTY;
+import static roomescape.exception.ExceptionType.EMPTY_DATE;
+import static roomescape.exception.ExceptionType.EMPTY_NAME;
+import static roomescape.exception.ExceptionType.EMPTY_THEME;
+import static roomescape.exception.ExceptionType.EMPTY_TIME;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
@@ -26,19 +26,19 @@ void constructTest() {
         assertAll(
                 () -> assertThatThrownBy(() -> new Reservation(null, DEFAULT_DATE, DEFAULT_TIME, DEFAULT_THEME))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(NAME_EMPTY.getMessage()),
+                        .hasMessage(EMPTY_NAME.getMessage()),
 
                 () -> assertThatThrownBy(() -> new Reservation("name", null, DEFAULT_TIME, DEFAULT_THEME))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(DATE_EMPTY.getMessage()),
+                        .hasMessage(EMPTY_DATE.getMessage()),
 
                 () -> assertThatThrownBy(() -> new Reservation("name", DEFAULT_DATE, null, DEFAULT_THEME))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(TIME_EMPTY.getMessage()),
+                        .hasMessage(EMPTY_TIME.getMessage()),
 
                 () -> assertThatThrownBy(() -> new Reservation("name", DEFAULT_DATE, DEFAULT_TIME, null))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(THEME_EMPTY.getMessage())
+                        .hasMessage(EMPTY_THEME.getMessage())
         );
 
     }
diff --git a/src/test/java/roomescape/domain/ReservationTimeTest.java b/src/test/java/roomescape/domain/ReservationTimeTest.java
index e822f5646b..afc563831c 100644
--- a/src/test/java/roomescape/domain/ReservationTimeTest.java
+++ b/src/test/java/roomescape/domain/ReservationTimeTest.java
@@ -3,11 +3,11 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertAll;
+import static roomescape.exception.ExceptionType.EMPTY_TIME;
 
 import java.time.LocalTime;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
-import roomescape.exception.ExceptionType;
 import roomescape.exception.RoomescapeException;
 
 class ReservationTimeTest {
@@ -17,7 +17,7 @@ class ReservationTimeTest {
     void startAtMustBeNotNull() {
         assertThatThrownBy(() -> new ReservationTime(null))
                 .isInstanceOf(RoomescapeException.class)
-                .hasMessage(ExceptionType.TIME_EMPTY.getMessage());
+                .hasMessage(EMPTY_TIME.getMessage());
     }
 
     @DisplayName("ReservationTime 은 id 값으로만 동등성을 비교한다.")
diff --git a/src/test/java/roomescape/domain/ThemeTest.java b/src/test/java/roomescape/domain/ThemeTest.java
index 9d63424f52..a5b3e088d4 100644
--- a/src/test/java/roomescape/domain/ThemeTest.java
+++ b/src/test/java/roomescape/domain/ThemeTest.java
@@ -4,9 +4,9 @@
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertAll;
-import static roomescape.exception.ExceptionType.DESCRIPTION_EMPTY;
-import static roomescape.exception.ExceptionType.NAME_EMPTY;
-import static roomescape.exception.ExceptionType.THUMBNAIL_EMPTY;
+import static roomescape.exception.ExceptionType.EMPTY_DESCRIPTION;
+import static roomescape.exception.ExceptionType.EMPTY_NAME;
+import static roomescape.exception.ExceptionType.EMPTY_THUMBNAIL;
 
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -20,15 +20,15 @@ void constructTest() {
         assertAll(
                 () -> assertThatThrownBy(() -> new Theme(null, "description", "thumbnail"))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(NAME_EMPTY.getMessage()),
+                        .hasMessage(EMPTY_NAME.getMessage()),
 
                 () -> assertThatThrownBy(() -> new Theme("name", null, "thumbnail"))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(DESCRIPTION_EMPTY.getMessage()),
+                        .hasMessage(EMPTY_DESCRIPTION.getMessage()),
 
                 () -> assertThatThrownBy(() -> new Theme("name", "description", null))
                         .isInstanceOf(RoomescapeException.class)
-                        .hasMessage(THUMBNAIL_EMPTY.getMessage()),
+                        .hasMessage(EMPTY_THUMBNAIL.getMessage()),
 
                 () -> assertThatCode(() -> new Theme("name", "description", "thumbnail"))
                         .doesNotThrowAnyException(),
diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java
index 1bf6505187..095bf220bf 100644
--- a/src/test/java/roomescape/service/ReservationServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationServiceTest.java
@@ -5,10 +5,10 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION;
-import static roomescape.exception.ExceptionType.NAME_EMPTY;
-import static roomescape.exception.ExceptionType.PAST_TIME;
-import static roomescape.exception.ExceptionType.RESERVATION_TIME_NOT_FOUND;
-import static roomescape.exception.ExceptionType.THEME_NOT_FOUND;
+import static roomescape.exception.ExceptionType.EMPTY_NAME;
+import static roomescape.exception.ExceptionType.NOT_FOUND_RESERVATION_TIME;
+import static roomescape.exception.ExceptionType.NOT_FOUND_THEME;
+import static roomescape.exception.ExceptionType.PAST_TIME_RESERVATION;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
@@ -77,7 +77,7 @@ void createPastReservationFailTest() {
                 defaultTheme.getId()
         )))
                 .isInstanceOf(RoomescapeException.class)
-                .hasMessage(PAST_TIME.getMessage());
+                .hasMessage(PAST_TIME_RESERVATION.getMessage());
     }
 
     @DisplayName("존재하지 않는 시간에 대해 예약을 생성하면 예외가 발생한다.")
@@ -90,7 +90,7 @@ void createReservationWithTimeNotExistsTest() {
                 defaultTheme.getId()
         )))
                 .isInstanceOf(RoomescapeException.class)
-                .hasMessage(RESERVATION_TIME_NOT_FOUND.getMessage());
+                .hasMessage(NOT_FOUND_RESERVATION_TIME.getMessage());
     }
 
     @DisplayName("존재하지 않는 테마에 대해 예약을 생성하면 예외가 발생한다.")
@@ -103,7 +103,7 @@ void createReservationWithThemeNotExistsTest() {
                 2L
         )))
                 .isInstanceOf(RoomescapeException.class)
-                .hasMessage(THEME_NOT_FOUND.getMessage());
+                .hasMessage(NOT_FOUND_THEME.getMessage());
     }
 
     @DisplayName("필수값이 입력되지 않은 예약 생성 요청에 대해 예외가 발생한다.")
@@ -117,7 +117,7 @@ void emptyRequiredValueTest() {
                         defaultTime.getId(),
                         defaultTheme.getId()
                 ))).isInstanceOf(RoomescapeException.class)
-                        .hasMessage(NAME_EMPTY.getMessage())
+                        .hasMessage(EMPTY_NAME.getMessage())
         );
     }
 
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index cac7188ab2..53ac9e7fa5 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -3,8 +3,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static roomescape.exception.ExceptionType.DELETE_USED_TIME;
 import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME;
-import static roomescape.exception.ExceptionType.INVALID_DELETE_TIME;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
@@ -87,7 +87,7 @@ void usedReservationTimeDeleteTest() {
             //when & then
             assertThatCode(() -> reservationTimeService.delete(1L))
                     .isInstanceOf(RoomescapeException.class)
-                    .hasMessage(INVALID_DELETE_TIME.getMessage());
+                    .hasMessage(DELETE_USED_TIME.getMessage());
         }
     }
 
diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java
index 04220a1b6e..241d341168 100644
--- a/src/test/java/roomescape/service/ThemeServiceTest.java
+++ b/src/test/java/roomescape/service/ThemeServiceTest.java
@@ -3,8 +3,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static roomescape.exception.ExceptionType.DELETE_USED_THEME;
 import static roomescape.exception.ExceptionType.DUPLICATE_THEME;
-import static roomescape.exception.ExceptionType.INVALID_DELETE_THEME;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
@@ -88,7 +88,7 @@ void removeFailTest() {
             //when & then
             assertThatThrownBy(() -> themeService.delete(1L))
                     .isInstanceOf(RoomescapeException.class)
-                    .hasMessage(INVALID_DELETE_THEME.getMessage());
+                    .hasMessage(DELETE_USED_THEME.getMessage());
         }
 
         @DisplayName("존재하지 않는 테마 id로 삭제하더라도 오류로 간주하지 않는다.")

From 6ec052d45f0d9553c8a2e99f1262d5e7e4f818ca Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 15:41:15 +0900
Subject: [PATCH 50/75] =?UTF-8?q?refactor:=20ResultSet=20=EC=82=AC?=
 =?UTF-8?q?=EC=9A=A9=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcTemplateReservationRepository.java    | 37 ++++++++++---------
 1 file changed, 19 insertions(+), 18 deletions(-)

diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index 213d7ef6f4..33250ec189 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -2,10 +2,10 @@
 
 import java.sql.Date;
 import java.sql.PreparedStatement;
-import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
 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;
@@ -59,8 +59,8 @@ public List<Reservation> findAll() {
         String query = """
                    SELECT 
                    r.id as reservation_id,
-                   r.name,
-                   r.date,
+                   r.name as reservation_name,
+                   r.date as reservation_date,
                    t.id as time_id,
                    t.start_at as time_value,
                    t2.id as theme_id,
@@ -72,21 +72,22 @@ public List<Reservation> findAll() {
                 on r.time_id = t.id
                 inner join theme t2  
                 on t2.id = r.theme_id""";
-        return jdbcTemplate.query(query,
-                (rs, rowNum) -> {
-                    long id = rs.getLong(1);
-                    String name = rs.getString(2);
-                    LocalDate date = rs.getDate(3).toLocalDate();
-                    long timeId = rs.getLong(4);
-                    LocalTime startAt = rs.getTime(5).toLocalTime();
-                    ReservationTime reservationTime = new ReservationTime(timeId, startAt);
-                    long themeId = rs.getLong("theme_id");
-                    String themeName = rs.getString("theme_name");
-                    String description = rs.getString("description");
-                    String thumbnail = rs.getString("thumbnail");
-                    Theme theme = new Theme(themeId, themeName, description, thumbnail);
-                    return new Reservation(id, name, date, reservationTime, theme);
-                });
+        RowMapper<Reservation> reservationRowMapper = (rs, rowNum) -> new Reservation(
+                rs.getLong("reservation_id"),
+                rs.getString("reservation_name"),
+                rs.getDate("reservation_date").toLocalDate(),
+                new ReservationTime(
+                        rs.getLong("time_id"),
+                        rs.getTime("time_value").toLocalTime()
+                ),
+                new Theme(
+                        rs.getLong("theme_id"),
+                        rs.getString("theme_name"),
+                        rs.getString("description"),
+                        rs.getString("thumbnail")
+                )
+        );
+        return jdbcTemplate.query(query, reservationRowMapper);
     }
 
     @Override

From ad587b545771c51973c6168831e117e3330632f4 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:03:11 +0900
Subject: [PATCH 51/75] =?UTF-8?q?docs:=20=ED=95=B4=EA=B2=B0=ED=95=9C=20?=
 =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EA=B0=80=EC=9D=B4=EB=93=9C=20Todo=20?=
 =?UTF-8?q?=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/resources/static/js/ranking.js          |  7 -------
 src/main/resources/static/js/reservation-new.js  |  4 ----
 src/main/resources/static/js/user-reservation.js | 16 +---------------
 3 files changed, 1 insertion(+), 26 deletions(-)

diff --git a/src/main/resources/static/js/ranking.js b/src/main/resources/static/js/ranking.js
index c4b20948b1..ba5dda7a9a 100644
--- a/src/main/resources/static/js/ranking.js
+++ b/src/main/resources/static/js/ranking.js
@@ -1,7 +1,4 @@
 document.addEventListener('DOMContentLoaded', () => {
-  /*
-  TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출
-  */
   const today = new Date();
   let startDate = formatDate(minusDay(today, 7));
   let endDate = formatDate(minusDay(today, 1));
@@ -26,10 +23,6 @@ function formatDate(date) {
 function render(data) {
   const container = document.getElementById('theme-ranking');
 
-  /*
-  TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출 후 렌더링
-        response 명세에 맞춰 name, thumbnail, description 값 설정
-  */
   data.forEach(theme => {
     const name = theme.name;
     const thumbnail = theme.thumbnail;
diff --git a/src/main/resources/static/js/reservation-new.js b/src/main/resources/static/js/reservation-new.js
index 2d8e132cce..fd87452bf6 100644
--- a/src/main/resources/static/js/reservation-new.js
+++ b/src/main/resources/static/js/reservation-new.js
@@ -23,10 +23,6 @@ function render(data) {
   data.forEach(item => {
     const row = tableBody.insertRow();
 
-    /*
-    TODO: [2단계] 관리자 기능 - 예약 목록 조회 API 호출 후 렌더링
-          response 명세에 맞춰 값 설정
-    */
     console.log(item);
     row.insertCell(0).textContent = item.id;            // 예약 id
     row.insertCell(1).textContent = item.name;          // 예약자명
diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js
index a0bcaed178..12a26c866a 100644
--- a/src/main/resources/static/js/user-reservation.js
+++ b/src/main/resources/static/js/user-reservation.js
@@ -38,11 +38,6 @@ function renderTheme(themes) {
   themes.forEach(theme => {
     const name = theme.name;
     const themeId = theme.id;
-    /*
-    TODO: [3단계] 사용자 예약 - 테마 목록 조회 API 호출 후 렌더링
-          response 명세에 맞춰 createSlot 함수 호출 시 값 설정
-          createSlot('theme', theme name, theme id) 형태로 호출
-    */
     themeSlots.appendChild(createSlot('theme', name, themeId));
   });
 }
@@ -87,10 +82,6 @@ function checkDateAndTheme() {
 }
 
 function fetchAvailableTimes(date, themeId) {
-  /*
-  TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출
-        요청 포맷에 맞게 설정
-  */
   fetch(`/availableTimes?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
     method: 'GET',
     headers: {
@@ -116,10 +107,6 @@ function renderAvailableTimes(times) {
     return;
   }
   times.forEach(time => {
-    /*
-    TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 후 렌더링
-          response 명세에 맞춰 createSlot 함수 호출 시 값 설정
-    */
     const startAt = time.startAt;
     const timeId = time.id;
     const alreadyBooked = time.isBooked;
@@ -158,8 +145,7 @@ function onReservationButtonClick() {
   if (selectedDate && selectedThemeId && selectedTimeId) {
 
     /*
-    TODO: [3단계] 사용자 예약 - 예약 요청 API 호출
-          [5단계] 예약 생성 기능 변경 - 사용자
+    TODO: [5단계] 예약 생성 기능 변경 - 사용자
           request 명세에 맞게 설정
     */
     const reservationData = {

From ddf88b1a7bad4fb1031e206a1488c84c36d430e6 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:05:01 +0900
Subject: [PATCH 52/75] =?UTF-8?q?docs:=20"TODO=20:=20NAME=5FEMPYT=20?=
 =?UTF-8?q?=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EA=B3=A0=EB=AF=BC"=20=EC=82=AD?=
 =?UTF-8?q?=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 이름이 비어있지 않아야 한다는 조건은 모든 도메인이 공통적으로 가질 가능성이 그렇지 않을 가능성보다 훨씬 높다고 판단함.
---
 src/main/java/roomescape/domain/Theme.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index 82483a5a3f..7694a2a092 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -29,7 +29,6 @@ public Theme(Long id, String name, String description, String thumbnail) {
 
     private void validateName(String name) {
         if (name == null || name.isBlank()) {
-            //TODO : NAME_EMPYT 재사용 고민
             throw new RoomescapeException(EMPTY_NAME);
         }
     }

From e36c4974347c35728581d5350e822e39eec612eb Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:08:18 +0900
Subject: [PATCH 53/75] =?UTF-8?q?docs:=20"//todo=20:=20long=20=EC=9C=BC?=
 =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD"=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 식별자가 null 인 경우 이 getId 메서드를 호출할 때 예외가 발생하는 것이 타당하다고 판단.
---
 src/main/java/roomescape/domain/ReservationTime.java | 8 ++++----
 src/main/java/roomescape/domain/Theme.java           | 3 +--
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/main/java/roomescape/domain/ReservationTime.java b/src/main/java/roomescape/domain/ReservationTime.java
index 50f22a934e..7ca9dd0b5f 100644
--- a/src/main/java/roomescape/domain/ReservationTime.java
+++ b/src/main/java/roomescape/domain/ReservationTime.java
@@ -26,7 +26,7 @@ public boolean isIdOf(long id) {
         return this.id == id;
     }
 
-    public Long getId() {
+    public long getId() {
         return id;
     }
 
@@ -58,8 +58,8 @@ public boolean equals(Object o) {
     @Override
     public String toString() {
         return "ReservationTime{" +
-                "id=" + id +
-                ", startAt=" + startAt +
-                '}';
+               "id=" + id +
+               ", startAt=" + startAt +
+               '}';
     }
 }
diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index 7694a2a092..83f72ce50b 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -57,8 +57,7 @@ public boolean isNameOf(String name) {
         return this.name.equals(name);
     }
 
-    //todo : long 으로 변경
-    public Long getId() {
+    public long getId() {
         return id;
     }
 

From 3961bb6e3209b8a39e2a8b0dfd33001845a692aa Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:09:47 +0900
Subject: [PATCH 54/75] =?UTF-8?q?docs:=20"//todo=20:=20=EB=B3=80=EC=88=98?=
 =?UTF-8?q?=EB=AA=85=20=EA=B3=A0=EB=AF=BC"=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 변수명을 변경하는 것 대신 메서드를 분리하는 것이 더 가독성이 향상된다고 판단
---
 .../roomescape/service/ReservationTimeService.java     | 10 ++++++----
 src/main/java/roomescape/service/ThemeService.java     | 10 ++++++----
 2 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 31584562a7..697bd24a99 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -47,12 +47,14 @@ public List<ReservationTimeResponse> findAll() {
     public void delete(long id) {
         //todo SQL로 구현
         List<Reservation> reservations = reservationRepository.findAll();
-        //TODO : 지역변수 네이밍 고민
-        boolean invalidDelete = reservations.stream()
-                .anyMatch(reservation -> reservation.isReservationTimeOf(id));
-        if (invalidDelete) {
+        if (isUsedTime(id, reservations)) {
             throw new RoomescapeException(DELETE_USED_TIME);
         }
         reservationTimeRepository.delete(id);
     }
+
+    private static boolean isUsedTime(long id, List<Reservation> reservations) {
+        return reservations.stream()
+                .anyMatch(reservation -> reservation.isReservationTimeOf(id));
+    }
 }
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index 9f355f8b71..d15bfff70a 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -52,12 +52,14 @@ public List<ThemeResponse> findAndOrderByPopularity(LocalDate start, LocalDate e
     }
 
     public void delete(long id) {
-        //todo : 변수명 고민
-        boolean invalidDelete = reservationRepository.findAll().stream()
-                .anyMatch(reservation -> reservation.isThemeOf(id));
-        if (invalidDelete) {
+        if (isUsedTheme(id)) {
             throw new RoomescapeException(DELETE_USED_THEME);
         }
         themeRepository.delete(id);
     }
+
+    private boolean isUsedTheme(long id) {
+        return reservationRepository.findAll().stream()
+                .anyMatch(reservation -> reservation.isThemeOf(id));
+    }
 }

From 1378a922285ffef5b20ca8a55b2c12a1c40e7255 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:10:10 +0900
Subject: [PATCH 55/75] =?UTF-8?q?docs:=20"//TODO=20:=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1"=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 이미 작성함.
---
 src/main/java/roomescape/service/ReservationTimeService.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 697bd24a99..893ea7eac6 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -43,7 +43,6 @@ public List<ReservationTimeResponse> findAll() {
                 .toList();
     }
 
-    //TODO : 테스트 생성
     public void delete(long id) {
         //todo SQL로 구현
         List<Reservation> reservations = reservationRepository.findAll();

From 093f68b69640d5fb3910dad79dfcc08296cad4eb Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:13:07 +0900
Subject: [PATCH 56/75] =?UTF-8?q?docs:=20"//Todo=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1"=20=EC=82=AD?=
 =?UTF-8?q?=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Controller 는 서비스의 코드를 그대로 호출하므로 테스트 작성이 필요하지 않다고 판단
- 단, 기존의 잘 동작하는 테스트 코드를 삭제하지는 않음
---
 src/main/java/roomescape/controller/ThemeController.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/controller/ThemeController.java b/src/main/java/roomescape/controller/ThemeController.java
index dbb0cb2520..73c765d680 100644
--- a/src/main/java/roomescape/controller/ThemeController.java
+++ b/src/main/java/roomescape/controller/ThemeController.java
@@ -16,7 +16,6 @@
 import roomescape.dto.ThemeResponse;
 import roomescape.service.ThemeService;
 
-//Todo 테스트코드 작성
 @RestController
 @RequestMapping("/themes")
 public class ThemeController {

From 1024c7e57e43ddaaf952ffac521002beafc6ce23 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:15:34 +0900
Subject: [PATCH 57/75] =?UTF-8?q?docs:=20"todo=20[=EB=A1=9C=EB=B9=88]=20?=
 =?UTF-8?q?=EC=9D=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9E=91?=
 =?UTF-8?q?=EC=84=B1=ED=95=98=EB=8B=A4=20=EB=B3=B4=EB=8B=88=20Response=20d?=
 =?UTF-8?q?to=20=EC=97=90=EB=8F=84=20NotNull=20=EA=B2=80=EC=A6=9D=EC=9D=80?=
 =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=A0=20=EA=B2=83=20=EA=B0=99=EB=8B=A4?=
 =?UTF-8?q?=EB=8A=94=20=EC=83=9D=EA=B0=81=EC=9D=B4=20=EB=93=AD=EB=8B=88?=
 =?UTF-8?q?=EB=8B=A4."=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 각자 추구하는 방향이 달라 페어 프로그래밍이 끝난 후 각자 개선하는 것으로 합의
---
 .../service/ReservationServiceTest.java       | 33 +++++++++----------
 1 file changed, 16 insertions(+), 17 deletions(-)

diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java
index 095bf220bf..b28d9bced8 100644
--- a/src/test/java/roomescape/service/ReservationServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationServiceTest.java
@@ -109,7 +109,6 @@ void createReservationWithThemeNotExistsTest() {
     @DisplayName("필수값이 입력되지 않은 예약 생성 요청에 대해 예외가 발생한다.")
     @Test
     void emptyRequiredValueTest() {
-        //todo [로빈] 이 테스트를 작성하다 보니 Response dto 에도 NotNull 검증은 필요할 것 같다는 생각이 듭니다.
         assertAll(
                 () -> assertThatThrownBy(() -> reservationService.save(new ReservationRequest(
                         LocalDate.now().minusDays(1),
@@ -121,6 +120,22 @@ void emptyRequiredValueTest() {
         );
     }
 
+    @DisplayName("예약이 여러 개 존재하는 경우 모든 예약을 조회할 수 있다.")
+    @Test
+    void findAllTest() {
+        //given
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(1), defaultTime, defaultTheme));
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(2), defaultTime, defaultTheme));
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(3), defaultTime, defaultTheme));
+        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(4), defaultTime, defaultTheme));
+
+        //when
+        List<ReservationResponse> reservationResponses = reservationService.findAll();
+
+        //then
+        assertThat(reservationResponses).hasSize(4);
+    }
+
     @DisplayName("예약이 하나 존재하는 경우")
     @Nested
     class OneReservationExistsTest {
@@ -160,20 +175,4 @@ void deleteNotExistReservationNotThrowsException() {
                     .doesNotThrowAnyException();
         }
     }
-
-    @DisplayName("예약이 여러 개 존재하는 경우 모든 예약을 조회할 수 있다.")
-    @Test
-    void findAllTest() {
-        //given
-        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(1), defaultTime, defaultTheme));
-        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(2), defaultTime, defaultTheme));
-        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(3), defaultTime, defaultTheme));
-        reservationRepository.save(new Reservation("name1", LocalDate.now().plusDays(4), defaultTime, defaultTheme));
-
-        //when
-        List<ReservationResponse> reservationResponses = reservationService.findAll();
-
-        //then
-        assertThat(reservationResponses).hasSize(4);
-    }
 }

From 258466cd2c6bb13d6be2e38f7a558689ff6bdadb Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:17:41 +0900
Subject: [PATCH 58/75] =?UTF-8?q?docs:=20"TODO=20:=20=EB=8F=84=EB=A9=94?=
 =?UTF-8?q?=EC=9D=B8=EC=97=90=EA=B2=8C=20=EB=84=98=EA=B8=B8=20=EC=88=98=20?=
 =?UTF-8?q?=EC=9E=88=EC=9D=84=20=EA=B2=83=20=EA=B0=99=EC=9D=80=EB=8D=B0"?=
 =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 도메인으로 이동할 수 있는 책임을 이동
---
 src/main/java/roomescape/domain/Reservation.java         | 8 ++++++--
 src/main/java/roomescape/service/ReservationService.java | 8 +-------
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index 9e0abaaf20..97f602d14f 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -70,8 +70,12 @@ public int compareTo(Reservation other) {
         return dateTime.compareTo(otherDateTime);
     }
 
-    public boolean hasSameId(long id) {
-        return this.id == id;
+    public boolean isBefore(LocalDateTime base) {
+        return LocalDateTime.of(date, getTime()).isBefore(base);
+    }
+
+    public LocalTime getTime() {
+        return time.getStartAt();
     }
 
     public boolean isReservationTimeOf(long id) {
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 12c392135b..7b030e71ff 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -53,7 +53,7 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
             throw new RoomescapeException(DUPLICATE_RESERVATION);
         }
 
-        if (isBefore(beforeSave)) {
+        if (beforeSave.isBefore(LocalDateTime.now())) {
             throw new RoomescapeException(PAST_TIME_RESERVATION);
         }
 
@@ -66,12 +66,6 @@ private boolean validateDuplicateReservation(Reservation beforeSave, Reservation
                 && beforeSave.isSameTheme(reservation);
     }
 
-    //TODO : 도메인에게 넘길 수 있을 것 같은데
-    private static boolean isBefore(Reservation beforeSave) {
-        return LocalDateTime.of(beforeSave.getDate(), beforeSave.getTime())
-                .isBefore(LocalDateTime.now());
-    }
-
     private ReservationResponse toResponse(Reservation reservation) {
         ReservationTime reservationTime = reservation.getReservationTime();
         ReservationTimeResponse reservationTimeResponse = new ReservationTimeResponse(reservationTime.getId(),

From 8c4efbc7f217dc1efeaecba8452415a8213dfc36 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:18:43 +0900
Subject: [PATCH 59/75] =?UTF-8?q?docs:=20"Todo=20:=20[=EB=A1=9C=EB=B9=88]?=
 =?UTF-8?q?=20Response=20=EA=B0=80=20=EB=B3=80=ED=99=98=20=EB=A1=9C?=
 =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EA=B0=80=EC=A7=80=EA=B3=A0=20=EC=9E=88?=
 =?UTF-8?q?=EC=9C=BC=EB=A9=B4=20=EC=95=84=EB=9E=98=20=EC=BD=94=EB=93=9C?=
 =?UTF-8?q?=EB=8F=84=20=EA=B0=84=EB=8B=A8=ED=95=B4=20=EC=A7=88=20=EA=B2=83?=
 =?UTF-8?q?=20=EA=B0=99=EC=9D=8C"=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 각자 추구하는 방향이 달라 페어 프로그래밍이 끝난 후 각자 개선하는 것으로 합의
---
 .../java/roomescape/controller/ReservationControllerTest.java    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index 5522db8150..277eedaba8 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -59,7 +59,6 @@ void saveReservation() {
         //then
         ReservationResponse expected = new ReservationResponse(id, "폴라", date,
                 new ReservationTimeResponse(TIME_ID, TIME),
-                //Todo : [로빈] Response 가 변환 로직을 가지고 있으면 아래 코드도 간단해 질 것 같음
                 new ThemeResponse(defualtTheme.getId(), defualtTheme.getName(), defualtTheme.getDescription(),
                         defualtTheme.getThumbnail()));
 

From c7c503332b1e1e6f9382e492e7e4dc9460ffbb72 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:22:48 +0900
Subject: [PATCH 60/75] =?UTF-8?q?refactor=20:=20=EB=8F=84=EB=A9=94?=
 =?UTF-8?q?=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=A0=20?=
 =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20?=
 =?UTF-8?q?=EC=9D=B4=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/roomescape/domain/Reservation.java    | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java
index 97f602d14f..0c8a38037f 100644
--- a/src/main/java/roomescape/domain/Reservation.java
+++ b/src/main/java/roomescape/domain/Reservation.java
@@ -78,6 +78,10 @@ public LocalTime getTime() {
         return time.getStartAt();
     }
 
+    public boolean hasSameId(long id) {
+        return this.id == id;
+    }
+
     public boolean isReservationTimeOf(long id) {
         return this.time.isIdOf(id);
     }
@@ -111,10 +115,6 @@ public LocalDate getDate() {
         return date;
     }
 
-    public LocalTime getTime() {
-        return time.getStartAt();
-    }
-
     public ReservationTime getReservationTime() {
         return time;
     }
@@ -149,10 +149,10 @@ public boolean equals(Object o) {
     @Override
     public String toString() {
         return "Reservation{" +
-                "id=" + id +
-                ", name='" + name + '\'' +
-                ", date=" + date +
-                ", time=" + time +
-                '}';
+               "id=" + id +
+               ", name='" + name + '\'' +
+               ", date=" + date +
+               ", time=" + time +
+               '}';
     }
 }

From 03918034014fd76503b1007a601ce0c86e44977d Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:23:56 +0900
Subject: [PATCH 61/75] =?UTF-8?q?docs:=20"Todo=20=EC=83=81=ED=83=9C?=
 =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B3=A0=EB=AF=BC=ED=95=B4=EB=B3=B4?=
 =?UTF-8?q?=EA=B8=B0"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 4~6 단계 인증 인가가 들어갈 때 고민이 해소 될 것이라 판단
---
 src/main/java/roomescape/exception/ExceptionType.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index 2ef6842495..e0d16e572c 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -4,7 +4,6 @@
 
 import org.springframework.http.HttpStatus;
 
-//Todo 상태코드 고민해보기 -> 4~6 단계 인증 인가가 들어갈 때를 대비
 public enum ExceptionType {
     EMPTY_NAME(BAD_REQUEST, "이름은 필수 값입니다."),
     EMPTY_TIME(BAD_REQUEST, "시작 시간은 필수 값입니다."),

From c7f2dad7ccbef7757328db8ca547c18f78920947 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:28:30 +0900
Subject: [PATCH 62/75] =?UTF-8?q?refactor:=20getter=20=EB=8C=80=EC=8B=A0?=
 =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?=
 =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/CollectionReservationTimeRepository.java         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index cc074089a3..a22896c61f 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -52,7 +52,7 @@ public List<ReservationTime> findAll() {
     @Override
     public void delete(long id) {
         reservationTimes.stream()
-                .filter(reservationTime -> reservationTime.getId().equals(id))
+                .filter(reservationTime -> reservationTime.isIdOf(id))
                 .findAny()
                 .ifPresent(reservationTimes::remove);
     }

From 241689654a96536eb151e471632a6072b90aeab1 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:29:28 +0900
Subject: [PATCH 63/75] =?UTF-8?q?docs:=20=ED=95=B4=EA=B2=B0=EB=90=9C=20tod?=
 =?UTF-8?q?o=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../roomescape/repository/JdbcTemplateReservationRepository.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index 33250ec189..b6619dee52 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -53,7 +53,6 @@ private void save(Reservation reservation, KeyHolder keyHolder) {
         }, keyHolder);
     }
 
-    //Todo 개선 고민
     @Override
     public List<Reservation> findAll() {
         String query = """

From e8e7a6f7678710f3ea07ad48abdb8dd2edfb3007 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Thu, 2 May 2024 16:31:32 +0900
Subject: [PATCH 64/75] =?UTF-8?q?style:=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?=
 =?UTF-8?q?=EC=A0=81=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcTemplateThemeRepository.java          |  3 +-
 .../service/ReservationService.java           |  2 +-
 .../service/ReservationTimeServiceTest.java   | 34 +++++++++----------
 .../roomescape/service/ThemeServiceTest.java  | 32 ++++++++---------
 4 files changed, 35 insertions(+), 36 deletions(-)

diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
index 497b82c526..310633913d 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
@@ -14,14 +14,13 @@
 @Repository
 public class JdbcTemplateThemeRepository implements ThemeRepository {
     private final JdbcTemplate jdbcTemplate;
-    private RowMapper<Theme> themeRowMapper = (rs, rowNum) -> {
+    private final RowMapper<Theme> themeRowMapper = (rs, rowNum) -> {
         long id = rs.getLong("id");
         String name = rs.getString("name");
         String description = rs.getString("description");
         String thumbnail = rs.getString("thumbnail");
         return new Theme(id, name, description, thumbnail);
     };
-    ;
 
     public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 7b030e71ff..4d99050d83 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -63,7 +63,7 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
 
     private boolean validateDuplicateReservation(Reservation beforeSave, Reservation reservation) {
         return reservation.isSameDateTime(beforeSave)
-                && beforeSave.isSameTheme(reservation);
+               && beforeSave.isSameTheme(reservation);
     }
 
     private ReservationResponse toResponse(Reservation reservation) {
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index 53ac9e7fa5..0b0cd90642 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -35,6 +35,23 @@ void initService() {
         reservationTimeService = new ReservationTimeService(reservationRepository, reservationTimeRepository);
     }
 
+    @DisplayName("저장된 시간을 모두 조회할 수 있다.")
+    @Test
+    void findAllTest() {
+        //given
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(10, 0)));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(11, 0)));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 0)));
+        reservationTimeRepository.save(new ReservationTime(LocalTime.of(13, 0)));
+
+        //when
+        List<ReservationTimeResponse> reservationTimeResponses = reservationTimeService.findAll();
+
+        //then
+        assertThat(reservationTimeResponses)
+                .hasSize(4);
+    }
+
     @DisplayName("예약 시간이 하나 존재할 때")
     @Nested
     class OneReservationTimeExists {
@@ -90,21 +107,4 @@ void usedReservationTimeDeleteTest() {
                     .hasMessage(DELETE_USED_TIME.getMessage());
         }
     }
-
-    @DisplayName("저장된 시간을 모두 조회할 수 있다.")
-    @Test
-    void findAllTest() {
-        //given
-        reservationTimeRepository.save(new ReservationTime(LocalTime.of(10, 0)));
-        reservationTimeRepository.save(new ReservationTime(LocalTime.of(11, 0)));
-        reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 0)));
-        reservationTimeRepository.save(new ReservationTime(LocalTime.of(13, 0)));
-
-        //when
-        List<ReservationTimeResponse> reservationTimeResponses = reservationTimeService.findAll();
-
-        //then
-        assertThat(reservationTimeResponses)
-                .hasSize(4);
-    }
 }
diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java
index 241d341168..8e1505d457 100644
--- a/src/test/java/roomescape/service/ThemeServiceTest.java
+++ b/src/test/java/roomescape/service/ThemeServiceTest.java
@@ -40,6 +40,22 @@ void initService() {
         themeService = new ThemeService(themeRepository, reservationRepository);
     }
 
+    @DisplayName("테마가 여러개 있으면 테마를 모두 조회할 수 있다.")
+    @Test
+    void findAllTest() {
+        //given
+        themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
+        themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
+        themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
+        themeRepository.save(new Theme("name4", "description4", "thumbnail4"));
+
+        //when
+        List<ThemeResponse> themeResponses = themeService.findAll();
+
+        //then
+        assertThat(themeResponses).hasSize(4);
+    }
+
     @DisplayName("테마, 시간이 하나 존재할 때")
     @Nested
     class OneThemeTest {
@@ -98,20 +114,4 @@ void notExistThemeDeleteTest() {
                     .doesNotThrowAnyException();
         }
     }
-
-    @DisplayName("테마가 여러개 있으면 테마를 모두 조회할 수 있다.")
-    @Test
-    void findAllTest() {
-        //given
-        themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
-        themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
-        themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
-        themeRepository.save(new Theme("name4", "description4", "thumbnail4"));
-
-        //when
-        List<ThemeResponse> themeResponses = themeService.findAll();
-
-        //then
-        assertThat(themeResponses).hasSize(4);
-    }
 }

From 62cef24c731d38fafba18ec5b6e144a94ef77814 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Sat, 4 May 2024 16:00:05 +0900
Subject: [PATCH 65/75] =?UTF-8?q?docs:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?=
 =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=ED=91=9C=EC=8B=9C=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index fd820a31e5..f3265c68e4 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
     - [x] 중복된 이름의 테마 생성 요청시 에러
     - [x] 예약이 있는 테마를 삭제 요청시 에러
 
-- [ ] 사용자 예약 기능 추가
+- [x] 사용자 예약 기능 추가
 - [x] 인기 테마 기능 추가
 
 # API 명세

From 1561f7ed7df81ad31d1580392fc9a0b631c314a0 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Mon, 6 May 2024 01:10:25 +0900
Subject: [PATCH 66/75] =?UTF-8?q?docs:=20=EB=A6=AC=ED=8C=A9=ED=86=A0?=
 =?UTF-8?q?=EB=A7=81=20=EB=8C=80=EC=83=81=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95?=
 =?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 REFACTORING_LIST.md | 12 ++++++++++++
 1 file changed, 12 insertions(+)
 create mode 100644 REFACTORING_LIST.md

diff --git a/REFACTORING_LIST.md b/REFACTORING_LIST.md
new file mode 100644
index 0000000000..62a7673edf
--- /dev/null
+++ b/REFACTORING_LIST.md
@@ -0,0 +1,12 @@
+## 리팩토링 대상 목록
+
+### 우선순위 높음
+
+1. Repository.findAll() + Stream 을 사용한 코드
+2. SQL 표준 작성 방식에 따라 대소문자 통일
+3. Repository 의 인메모리 구현체 대신 다른 종류의 테스트 더블 사용하기
+
+### 우선순위 낮음
+
+1. Service의 일부 검증 로직을 도메인으로 이동
+    - 조회 도메인과 생성 도메인을 분리

From c9b2f37a3376e6597ac39494a7d9d381e0cf1700 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Mon, 6 May 2024 02:12:05 +0900
Subject: [PATCH 67/75] =?UTF-8?q?refactor:=20RowMapper=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20?=
 =?UTF-8?q?=EB=B0=8F=20sql=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=ED=86=B5?=
 =?UTF-8?q?=EC=9D=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcTemplateReservationRepository.java    | 70 ++++++-------------
 ...JdbcTemplateReservationTimeRepository.java | 52 +++++++-------
 .../JdbcTemplateThemeRepository.java          | 46 ++++++------
 .../rowmapper/ReservationRowMapper.java       | 31 ++++++++
 .../rowmapper/ReservationTimeRowMapper.java   | 18 +++++
 .../repository/rowmapper/ThemeRowMapper.java  | 19 +++++
 6 files changed, 136 insertions(+), 100 deletions(-)
 create mode 100644 src/main/java/roomescape/repository/rowmapper/ReservationRowMapper.java
 create mode 100644 src/main/java/roomescape/repository/rowmapper/ReservationTimeRowMapper.java
 create mode 100644 src/main/java/roomescape/repository/rowmapper/ThemeRowMapper.java

diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index b6619dee52..e19fd98616 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -2,48 +2,35 @@
 
 import java.sql.Date;
 import java.sql.PreparedStatement;
-import java.time.LocalTime;
 import java.util.List;
 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.domain.Reservation;
-import roomescape.domain.ReservationTime;
-import roomescape.domain.Theme;
+import roomescape.repository.rowmapper.ReservationRowMapper;
 
 @Repository
 public class JdbcTemplateReservationRepository implements ReservationRepository {
     private final JdbcTemplate jdbcTemplate;
+    private final ReservationRowMapper reservationRowMapper;
 
-    public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate) {
+    public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate, ReservationRowMapper reservationRowMapper) {
         this.jdbcTemplate = jdbcTemplate;
+        this.reservationRowMapper = reservationRowMapper;
     }
 
     @Override
     public Reservation save(Reservation reservation) {
-        ReservationTime reservationTime = findReservationTime(reservation.getReservationTime().getId());
-        Reservation beforeSaved = new Reservation(null, reservation.getName(), reservation.getDate(),
-                reservationTime, reservation.getTheme());
         KeyHolder keyHolder = new GeneratedKeyHolder();
-        save(beforeSaved, keyHolder);
+        save(reservation, keyHolder);
         long id = keyHolder.getKey().longValue();
         return new Reservation(id, reservation);
     }
 
-    private ReservationTime findReservationTime(long timeId) {
-        String reservationTimeSelectSql = "select * from reservation_time where id = ?";
-        return jdbcTemplate.queryForObject(reservationTimeSelectSql, (rs, rowNum) -> {
-            long id = rs.getLong(1);
-            LocalTime startAt = rs.getTime(2).toLocalTime();
-            return new ReservationTime(id, startAt);
-        }, timeId);
-    }
-
     private void save(Reservation reservation, KeyHolder keyHolder) {
         jdbcTemplate.update(con -> {
-            String sql = "insert into reservation(name,date,time_id,THEME_ID) values ( ?,?,?,? )";
+            String sql = "INSERT INTO reservation(name, date, time_id, theme_id) VALUES ( ?,?,?,? )";
             PreparedStatement preparedStatement = con.prepareStatement(sql, new String[]{"id"});
             preparedStatement.setString(1, reservation.getName());
             preparedStatement.setDate(2, Date.valueOf(reservation.getDate()));
@@ -57,40 +44,25 @@ private void save(Reservation reservation, KeyHolder keyHolder) {
     public List<Reservation> findAll() {
         String query = """
                    SELECT 
-                   r.id as reservation_id,
-                   r.name as reservation_name,
-                   r.date as reservation_date,
-                   t.id as time_id,
-                   t.start_at as time_value,
-                   t2.id as theme_id,
-                   t2.NAME as theme_name,
-                   t2.DESCRIPTION as description,
-                   t2.THUMBNAIL as thumbnail
-                FROM reservation as r
-                inner join reservation_time t 
-                on r.time_id = t.id
-                inner join theme t2  
-                on t2.id = r.theme_id""";
-        RowMapper<Reservation> reservationRowMapper = (rs, rowNum) -> new Reservation(
-                rs.getLong("reservation_id"),
-                rs.getString("reservation_name"),
-                rs.getDate("reservation_date").toLocalDate(),
-                new ReservationTime(
-                        rs.getLong("time_id"),
-                        rs.getTime("time_value").toLocalTime()
-                ),
-                new Theme(
-                        rs.getLong("theme_id"),
-                        rs.getString("theme_name"),
-                        rs.getString("description"),
-                        rs.getString("thumbnail")
-                )
-        );
+                   r.id AS reservation_id,
+                   r.name AS reservation_name,
+                   r.date AS reservation_date,
+                   t.id AS time_id,
+                   t.start_at AS time_value,
+                   t2.id AS theme_id,
+                   t2.name AS theme_name,
+                   t2.description AS description,
+                   t2.thumbnail AS thumbnail
+                FROM reservation AS r
+                INNER JOIN reservation_time t 
+                ON r.time_id = t.id
+                INNER JOIN theme t2  
+                ON t2.id = r.theme_id""";
         return jdbcTemplate.query(query, reservationRowMapper);
     }
 
     @Override
     public void delete(long id) {
-        jdbcTemplate.update("delete from reservation where id = ?", id);
+        jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id);
     }
 }
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
index 9b6685bb52..814c9c06a9 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -10,59 +10,59 @@
 import org.springframework.jdbc.support.KeyHolder;
 import org.springframework.stereotype.Repository;
 import roomescape.domain.ReservationTime;
+import roomescape.repository.rowmapper.ReservationTimeRowMapper;
 
 @Repository
 public class JdbcTemplateReservationTimeRepository implements ReservationTimeRepository {
     private final JdbcTemplate jdbcTemplate;
+    private final ReservationTimeRowMapper reservationTimeRowMapper;
 
-    public JdbcTemplateReservationTimeRepository(JdbcTemplate jdbcTemplate) {
+    public JdbcTemplateReservationTimeRepository(JdbcTemplate jdbcTemplate,
+                                                 ReservationTimeRowMapper reservationTimeRowMapper) {
         this.jdbcTemplate = jdbcTemplate;
+        this.reservationTimeRowMapper = reservationTimeRowMapper;
     }
 
     @Override
     public ReservationTime save(ReservationTime reservationTime) {
         KeyHolder keyHolder = new GeneratedKeyHolder();
         save(reservationTime, keyHolder);
-        return new ReservationTime(keyHolder.getKey().longValue(), reservationTime.getStartAt());
+        long id = keyHolder.getKey().longValue();
+        return new ReservationTime(id, reservationTime.getStartAt());
+    }
+
+    private void save(ReservationTime reservationTime, KeyHolder keyHolder) {
+        jdbcTemplate.update(con -> {
+            String sql = "INSERT INTO reservation_time(start_at) VALUES ( ? )";
+            PreparedStatement pstmt = con.prepareStatement(sql, new String[]{"id"});
+            pstmt.setTime(1, Time.valueOf(reservationTime.getStartAt()));
+            return pstmt;
+        }, keyHolder);
     }
 
     @Override
     public boolean existsByStartAt(LocalTime startAt) {
-        return jdbcTemplate.queryForObject(
-                "select exists(select 1 from RESERVATION_TIME where START_AT = ?)",
-                Boolean.class, startAt);
+        String sql = "SELECT EXISTS(SELECT 1 FROM reservation_time WHERE start_at = ?)";
+        return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, startAt));
     }
 
     @Override
     public Optional<ReservationTime> findById(long id) {
-        List<ReservationTime> times = jdbcTemplate.query("select start_at from reservation_time where id = ?",
-                (rs, rowNum) -> {
-                    LocalTime time = rs.getTime(1).toLocalTime();
-                    return new ReservationTime(id, time);
-                }, id);
-        return times.stream().findFirst();
+        String sql = "SELECT id, start_at FROM reservation_time WHERE id = ?";
+        return jdbcTemplate.query(sql, reservationTimeRowMapper, id)
+                .stream()
+                .findAny();
     }
 
     @Override
     public List<ReservationTime> findAll() {
-        return jdbcTemplate.query("select * from reservation_time", (rs, rowNum) -> {
-            long id = rs.getLong(1);
-            LocalTime time = rs.getTime(2).toLocalTime();
-            return new ReservationTime(id, time);
-        });
+        String sql = "SELECT id, start_at FROM reservation_time";
+        return jdbcTemplate.query(sql, reservationTimeRowMapper);
     }
 
     @Override
     public void delete(long id) {
-        jdbcTemplate.update("delete from reservation_time where id = ?", id);
-    }
-
-    private void save(ReservationTime reservationTime, KeyHolder keyHolder) {
-        jdbcTemplate.update(con -> {
-            PreparedStatement pstmt = con.prepareStatement("insert into reservation_time(start_at) values ( ? )",
-                    new String[]{"id"});
-            pstmt.setTime(1, Time.valueOf(reservationTime.getStartAt()));
-            return pstmt;
-        }, keyHolder);
+        String sql = "DELETE FROM reservation_time WHERE id = ?";
+        jdbcTemplate.update(sql, id);
     }
 }
diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
index 310633913d..dd95c751cb 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
@@ -5,44 +5,40 @@
 import java.util.List;
 import java.util.Optional;
 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.domain.Theme;
+import roomescape.repository.rowmapper.ThemeRowMapper;
 
 @Repository
 public class JdbcTemplateThemeRepository implements ThemeRepository {
     private final JdbcTemplate jdbcTemplate;
-    private final RowMapper<Theme> themeRowMapper = (rs, rowNum) -> {
-        long id = rs.getLong("id");
-        String name = rs.getString("name");
-        String description = rs.getString("description");
-        String thumbnail = rs.getString("thumbnail");
-        return new Theme(id, name, description, thumbnail);
-    };
+    private final ThemeRowMapper themeRowMapper;
 
-    public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate) {
+    public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate, ThemeRowMapper themeRowMapper) {
         this.jdbcTemplate = jdbcTemplate;
+        this.themeRowMapper = themeRowMapper;
     }
 
     @Override
     public List<Theme> findAll() {
-        return jdbcTemplate.query("select ID, NAME, DESCRIPTION, THUMBNAIL from THEME", themeRowMapper);
+        String sql = "SELECT id, name, description, thumbnail FROM theme";
+        return jdbcTemplate.query(sql, themeRowMapper);
     }
 
     @Override
     public List<Theme> findAndOrderByPopularity(LocalDate start, LocalDate end, int count) {
-        return jdbcTemplate.query(
-                "select th.*, count(*) as count from theme th join reservation r on r.theme_id = th.id where PARSEDATETIME(r.date,'yyyy-MM-dd') >= PARSEDATETIME(?,'yyyy-MM-dd') and PARSEDATETIME(r.date,'yyyy-MM-dd') <= PARSEDATETIME(?,'yyyy-MM-dd') group by th.id order by count desc limit ?",
-                themeRowMapper, start, end, count);
+        String sql = "SELECT th.id AS id, th.name AS name, description, thumbnail, COUNT(*) AS count FROM theme th JOIN reservation r ON r.theme_id = th.id WHERE PARSEDATETIME(r.date,'yyyy-MM-dd') >= PARSEDATETIME(?,'yyyy-MM-dd') AND PARSEDATETIME(r.date,'yyyy-MM-dd') <= PARSEDATETIME(?,'yyyy-MM-dd') GROUP BY th.id ORDER BY count DESC LIMIT ?";
+        return jdbcTemplate.query(sql, themeRowMapper, start, end, count);
     }
 
     @Override
     public Optional<Theme> findById(long id) {
-        List<Theme> themes = jdbcTemplate.query("select id, name, description, thumbnail from theme where id = ?",
-                themeRowMapper, id);
-        return themes.stream().findFirst();
+        String sql = "SELECT id, name, description, thumbnail FROM theme WHERE id = ?";
+        return jdbcTemplate.query(sql, themeRowMapper, id)
+                .stream()
+                .findAny();
     }
 
     @Override
@@ -55,18 +51,18 @@ public Theme save(Theme theme) {
 
     private void save(Theme theme, KeyHolder keyHolder) {
         jdbcTemplate.update(con -> {
-                    PreparedStatement pstm = con.prepareStatement(
-                            "insert into THEME (NAME, DESCRIPTION, THUMBNAIL) values (?, ?, ?) ", new String[]{"id"});
-                    pstm.setString(1, theme.getName());
-                    pstm.setString(2, theme.getDescription());
-                    pstm.setString(3, theme.getThumbnail());
-                    return pstm;
-                },
-                keyHolder);
+            String sql = "INSERT INTO theme (name, description, thumbnail) VALUES (?, ?, ?)";
+            PreparedStatement pstmt = con.prepareStatement(sql, new String[]{"id"});
+            pstmt.setString(1, theme.getName());
+            pstmt.setString(2, theme.getDescription());
+            pstmt.setString(3, theme.getThumbnail());
+            return pstmt;
+        }, keyHolder);
     }
 
     @Override
     public void delete(long id) {
-        jdbcTemplate.update("delete from THEME where id = ?", id);
+        String sql = "DELETE FROM theme WHERE id = ?";
+        jdbcTemplate.update(sql, id);
     }
 }
diff --git a/src/main/java/roomescape/repository/rowmapper/ReservationRowMapper.java b/src/main/java/roomescape/repository/rowmapper/ReservationRowMapper.java
new file mode 100644
index 0000000000..ac08ea7ad5
--- /dev/null
+++ b/src/main/java/roomescape/repository/rowmapper/ReservationRowMapper.java
@@ -0,0 +1,31 @@
+package roomescape.repository.rowmapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
+
+@Component
+public class ReservationRowMapper implements RowMapper<Reservation> {
+    @Override
+    public Reservation mapRow(ResultSet rs, int rowNum) throws SQLException {
+        return new Reservation(
+                rs.getLong("reservation_id"),
+                rs.getString("reservation_name"),
+                rs.getDate("reservation_date").toLocalDate(),
+                new ReservationTime(
+                        rs.getLong("time_id"),
+                        rs.getTime("time_value").toLocalTime()
+                ),
+                new Theme(
+                        rs.getLong("theme_id"),
+                        rs.getString("theme_name"),
+                        rs.getString("description"),
+                        rs.getString("thumbnail")
+                )
+        );
+    }
+}
diff --git a/src/main/java/roomescape/repository/rowmapper/ReservationTimeRowMapper.java b/src/main/java/roomescape/repository/rowmapper/ReservationTimeRowMapper.java
new file mode 100644
index 0000000000..11ef7468f1
--- /dev/null
+++ b/src/main/java/roomescape/repository/rowmapper/ReservationTimeRowMapper.java
@@ -0,0 +1,18 @@
+package roomescape.repository.rowmapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalTime;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import roomescape.domain.ReservationTime;
+
+@Component
+public class ReservationTimeRowMapper implements RowMapper<ReservationTime> {
+    @Override
+    public ReservationTime mapRow(ResultSet rs, int rowNum) throws SQLException {
+        Long id = rs.getLong("id");
+        LocalTime time = rs.getTime("start_at").toLocalTime();
+        return new ReservationTime(id, time);
+    }
+}
diff --git a/src/main/java/roomescape/repository/rowmapper/ThemeRowMapper.java b/src/main/java/roomescape/repository/rowmapper/ThemeRowMapper.java
new file mode 100644
index 0000000000..30a1705d93
--- /dev/null
+++ b/src/main/java/roomescape/repository/rowmapper/ThemeRowMapper.java
@@ -0,0 +1,19 @@
+package roomescape.repository.rowmapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import roomescape.domain.Theme;
+
+@Component
+public class ThemeRowMapper implements RowMapper<Theme> {
+    @Override
+    public Theme mapRow(ResultSet rs, int rowNum) throws SQLException {
+        long id = rs.getLong("id");
+        String name = rs.getString("name");
+        String description = rs.getString("description");
+        String thumbnail = rs.getString("thumbnail");
+        return new Theme(id, name, description, thumbnail);
+    }
+}

From 92e851928a7775a2290e4ecf7cb588415579c675 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Mon, 6 May 2024 03:34:03 +0900
Subject: [PATCH 68/75] =?UTF-8?q?refactor:=20Stream=20=EB=8C=80=EC=8B=A0?=
 =?UTF-8?q?=20SQL=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcTemplateReservationRepository.java    | 25 ++++++++++++
 ...JdbcTemplateReservationTimeRepository.java | 15 +++++++
 .../repository/ReservationRepository.java     |  9 +++++
 .../repository/ReservationTimeRepository.java |  4 ++
 .../service/AvailableTimeService.java         | 39 ++++++++++---------
 .../service/ReservationService.java           | 27 +++++++------
 .../service/ReservationTimeService.java       | 21 +++++-----
 .../java/roomescape/service/ThemeService.java | 18 ++++++---
 .../CollectionReservationRepository.java      | 23 +++++++++++
 .../CollectionReservationTimeRepository.java  | 27 +++++++++++++
 .../service/AvailableTimeServiceTest.java     |  9 ++++-
 11 files changed, 169 insertions(+), 48 deletions(-)

diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index e19fd98616..d6dffca4f7 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -2,12 +2,15 @@
 
 import java.sql.Date;
 import java.sql.PreparedStatement;
+import java.time.LocalDate;
 import java.util.List;
 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.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 import roomescape.repository.rowmapper.ReservationRowMapper;
 
 @Repository
@@ -61,6 +64,28 @@ public List<Reservation> findAll() {
         return jdbcTemplate.query(query, reservationRowMapper);
     }
 
+    @Override
+    public boolean existsByThemeAndDateAndTime(Theme theme, LocalDate date, ReservationTime reservationTime) {
+        String sql = "SELECT EXISTS(SELECT 1 FROM reservation WHERE theme_id = ? AND date = ? AND time_id = ?)";
+        long themeId = theme.getId();
+        long timeId = reservationTime.getId();
+        return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, themeId, date, timeId));
+    }
+
+    @Override
+    public boolean existsByTime(ReservationTime reservationTime) {
+        String sql = "SELECT EXISTS(SELECT 1 FROM reservation WHERE time_id = ?)";
+        long timeId = reservationTime.getId();
+        return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, timeId));
+    }
+
+    @Override
+    public boolean existsByTheme(Theme theme) {
+        String sql = "SELECT EXISTS(SELECT 1 FROM reservation WHERE theme_id = ?)";
+        long themeId = theme.getId();
+        return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, themeId));
+    }
+
     @Override
     public void delete(long id) {
         jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id);
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
index 814c9c06a9..da84250a1b 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -2,6 +2,7 @@
 
 import java.sql.PreparedStatement;
 import java.sql.Time;
+import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
 import java.util.Optional;
@@ -10,6 +11,7 @@
 import org.springframework.jdbc.support.KeyHolder;
 import org.springframework.stereotype.Repository;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 import roomescape.repository.rowmapper.ReservationTimeRowMapper;
 
 @Repository
@@ -60,6 +62,19 @@ public List<ReservationTime> findAll() {
         return jdbcTemplate.query(sql, reservationTimeRowMapper);
     }
 
+    @Override
+    public List<ReservationTime> findUsedTimeByDateAndTheme(LocalDate date, Theme theme) {
+        String sql = """
+                SELECT rt.id, start_at
+                FROM reservation_time rt
+                JOIN reservation r
+                    ON rt.id = r.time_id
+                WHERE r.date = ?
+                AND r.theme_id = ?
+                """;
+        return jdbcTemplate.query(sql, reservationTimeRowMapper, date, theme.getId());
+    }
+
     @Override
     public void delete(long id) {
         String sql = "DELETE FROM reservation_time WHERE id = ?";
diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java
index 1ac0fbd917..c224f4975a 100644
--- a/src/main/java/roomescape/repository/ReservationRepository.java
+++ b/src/main/java/roomescape/repository/ReservationRepository.java
@@ -1,12 +1,21 @@
 package roomescape.repository;
 
+import java.time.LocalDate;
 import java.util.List;
 import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 public interface ReservationRepository {
     Reservation save(Reservation reservation);
 
     List<Reservation> findAll();
 
+    boolean existsByThemeAndDateAndTime(Theme theme, LocalDate date, ReservationTime reservationTime);
+
+    boolean existsByTime(ReservationTime reservationTime);
+
+    boolean existsByTheme(Theme theme);
+
     void delete(long id);
 }
diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java
index ca04e207b9..e033a38451 100644
--- a/src/main/java/roomescape/repository/ReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java
@@ -1,9 +1,11 @@
 package roomescape.repository;
 
+import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
 import java.util.Optional;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 public interface ReservationTimeRepository {
     ReservationTime save(ReservationTime reservationTime);
@@ -14,5 +16,7 @@ public interface ReservationTimeRepository {
 
     List<ReservationTime> findAll();
 
+    List<ReservationTime> findUsedTimeByDateAndTheme(LocalDate date, Theme theme);
+
     void delete(long id);
 }
diff --git a/src/main/java/roomescape/service/AvailableTimeService.java b/src/main/java/roomescape/service/AvailableTimeService.java
index 909c6ad493..cd7d328b43 100644
--- a/src/main/java/roomescape/service/AvailableTimeService.java
+++ b/src/main/java/roomescape/service/AvailableTimeService.java
@@ -1,42 +1,43 @@
 package roomescape.service;
 
+import static roomescape.exception.ExceptionType.NOT_FOUND_THEME;
+
 import java.time.LocalDate;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.stream.Collectors;
 import org.springframework.stereotype.Service;
-import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 import roomescape.dto.AvailableTimeResponse;
-import roomescape.repository.ReservationRepository;
+import roomescape.exception.RoomescapeException;
 import roomescape.repository.ReservationTimeRepository;
+import roomescape.repository.ThemeRepository;
 
 @Service
 public class AvailableTimeService {
-    private final ReservationRepository reservationRepository;
     private final ReservationTimeRepository reservationTimeRepository;
+    private final ThemeRepository themeRepository;
 
-    public AvailableTimeService(ReservationRepository reservationRepository,
-                                ReservationTimeRepository reservationTimeRepository) {
-        this.reservationRepository = reservationRepository;
+    public AvailableTimeService(ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository) {
         this.reservationTimeRepository = reservationTimeRepository;
+        this.themeRepository = themeRepository;
     }
 
-    //todo : 메서드 개선
     public List<AvailableTimeResponse> findByThemeAndDate(LocalDate date, long themeId) {
-        Set<Long> alreadyReservedTimeIds = reservationRepository.findAll().stream()
-                .filter(reservation -> reservation.isDateOf(date))
-                .filter(reservation -> reservation.isThemeOf(themeId))
-                .map(Reservation::getReservationTime)
-                .map(ReservationTime::getId)
-                .collect(Collectors.toSet());
+        Theme theme = themeRepository.findById(themeId)
+                .orElseThrow(() -> new RoomescapeException(NOT_FOUND_THEME));
+
+        var alreadyUsedTimes = new HashSet<>(reservationTimeRepository.findUsedTimeByDateAndTheme(date, theme));
 
         return reservationTimeRepository.findAll().stream()
-                .map(reservationTime -> {
-                    long id = reservationTime.getId();
-                    boolean isBooked = alreadyReservedTimeIds.contains(id);
-                    return new AvailableTimeResponse(id, reservationTime.getStartAt(), isBooked);
-                })
+                .map(reservationTime -> toResponse(alreadyUsedTimes, reservationTime))
                 .toList();
     }
+
+    private AvailableTimeResponse toResponse(Set<ReservationTime> alreadyUsedTimes, ReservationTime reservationTime) {
+        boolean isBooked = alreadyUsedTimes.contains(reservationTime);
+        long id = reservationTime.getId();
+        return new AvailableTimeResponse(id, reservationTime.getStartAt(), isBooked);
+    }
 }
diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java
index 4d99050d83..a4160cebe8 100644
--- a/src/main/java/roomescape/service/ReservationService.java
+++ b/src/main/java/roomescape/service/ReservationService.java
@@ -5,9 +5,11 @@
 import static roomescape.exception.ExceptionType.NOT_FOUND_THEME;
 import static roomescape.exception.ExceptionType.PAST_TIME_RESERVATION;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.List;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
 import roomescape.domain.Theme;
@@ -21,6 +23,7 @@
 import roomescape.repository.ThemeRepository;
 
 @Service
+@Transactional
 public class ReservationService {
     private final ReservationRepository reservationRepository;
     private final ReservationTimeRepository reservationTimeRepository;
@@ -34,7 +37,6 @@ public ReservationService(ReservationRepository reservationRepository,
     }
 
     public ReservationResponse save(ReservationRequest reservationRequest) {
-
         ReservationTime requestedTime = reservationTimeRepository.findById(reservationRequest.timeId())
                 .orElseThrow(() -> new RoomescapeException(NOT_FOUND_RESERVATION_TIME));
         Theme requestedTheme = themeRepository.findById(reservationRequest.themeId())
@@ -46,24 +48,25 @@ public ReservationResponse save(ReservationRequest reservationRequest) {
                 requestedTime,
                 requestedTheme
         );
-        boolean isDuplicate = reservationRepository.findAll()
-                .stream()
-                .anyMatch(reservation -> validateDuplicateReservation(beforeSave, reservation));
+
+        validateDuplicateReservation(requestedTime, requestedTheme, beforeSave.getDate());
+        validatePastTimeReservation(beforeSave);
+
+        Reservation saved = reservationRepository.save(beforeSave);
+        return toResponse(saved);
+    }
+
+    private void validateDuplicateReservation(ReservationTime requestedTime, Theme requestedTheme, LocalDate date) {
+        boolean isDuplicate = reservationRepository.existsByThemeAndDateAndTime(requestedTheme, date, requestedTime);
         if (isDuplicate) {
             throw new RoomescapeException(DUPLICATE_RESERVATION);
         }
+    }
 
+    private void validatePastTimeReservation(Reservation beforeSave) {
         if (beforeSave.isBefore(LocalDateTime.now())) {
             throw new RoomescapeException(PAST_TIME_RESERVATION);
         }
-
-        Reservation saved = reservationRepository.save(beforeSave);
-        return toResponse(saved);
-    }
-
-    private boolean validateDuplicateReservation(Reservation beforeSave, Reservation reservation) {
-        return reservation.isSameDateTime(beforeSave)
-               && beforeSave.isSameTheme(reservation);
     }
 
     private ReservationResponse toResponse(Reservation reservation) {
diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java
index 893ea7eac6..b4e6b98289 100644
--- a/src/main/java/roomescape/service/ReservationTimeService.java
+++ b/src/main/java/roomescape/service/ReservationTimeService.java
@@ -5,7 +5,7 @@
 
 import java.util.List;
 import org.springframework.stereotype.Service;
-import roomescape.domain.Reservation;
+import org.springframework.transaction.annotation.Transactional;
 import roomescape.domain.ReservationTime;
 import roomescape.dto.ReservationTimeRequest;
 import roomescape.dto.ReservationTimeResponse;
@@ -14,6 +14,7 @@
 import roomescape.repository.ReservationTimeRepository;
 
 @Service
+@Transactional
 public class ReservationTimeService {
     private final ReservationRepository reservationRepository;
     private final ReservationTimeRepository reservationTimeRepository;
@@ -44,16 +45,18 @@ public List<ReservationTimeResponse> findAll() {
     }
 
     public void delete(long id) {
-        //todo SQL로 구현
-        List<Reservation> reservations = reservationRepository.findAll();
-        if (isUsedTime(id, reservations)) {
-            throw new RoomescapeException(DELETE_USED_TIME);
-        }
+        validateUsedTime(id);
         reservationTimeRepository.delete(id);
     }
 
-    private static boolean isUsedTime(long id, List<Reservation> reservations) {
-        return reservations.stream()
-                .anyMatch(reservation -> reservation.isReservationTimeOf(id));
+    private void validateUsedTime(long id) {
+        reservationTimeRepository.findById(id).ifPresent(this::validateUsedTime);
+    }
+
+    private void validateUsedTime(ReservationTime reservationTime) {
+        boolean existsByTime = reservationRepository.existsByTime(reservationTime);
+        if (existsByTime) {
+            throw new RoomescapeException(DELETE_USED_TIME);
+        }
     }
 }
diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java
index d15bfff70a..7556b42577 100644
--- a/src/main/java/roomescape/service/ThemeService.java
+++ b/src/main/java/roomescape/service/ThemeService.java
@@ -6,6 +6,7 @@
 import java.time.LocalDate;
 import java.util.List;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import roomescape.domain.Theme;
 import roomescape.dto.ThemeRequest;
 import roomescape.dto.ThemeResponse;
@@ -14,6 +15,7 @@
 import roomescape.repository.ThemeRepository;
 
 @Service
+@Transactional
 public class ThemeService {
 
     private final ThemeRepository themeRepository;
@@ -52,14 +54,18 @@ public List<ThemeResponse> findAndOrderByPopularity(LocalDate start, LocalDate e
     }
 
     public void delete(long id) {
-        if (isUsedTheme(id)) {
-            throw new RoomescapeException(DELETE_USED_THEME);
-        }
+        validateUsedTheme(id);
         themeRepository.delete(id);
     }
 
-    private boolean isUsedTheme(long id) {
-        return reservationRepository.findAll().stream()
-                .anyMatch(reservation -> reservation.isThemeOf(id));
+    private void validateUsedTheme(long id) {
+        themeRepository.findById(id).ifPresent(this::validateUsedTheme);
+    }
+
+    private void validateUsedTheme(Theme theme) {
+        boolean existsByTime = reservationRepository.existsByTheme(theme);
+        if (existsByTime) {
+            throw new RoomescapeException(DELETE_USED_THEME);
+        }
     }
 }
diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java
index 6673a8f600..391f8b157a 100644
--- a/src/test/java/roomescape/repository/CollectionReservationRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java
@@ -1,9 +1,12 @@
 package roomescape.repository;
 
+import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
 import roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 public class CollectionReservationRepository implements ReservationRepository {
     private final List<Reservation> reservations;
@@ -29,6 +32,26 @@ public List<Reservation> findAll() {
                 .toList();
     }
 
+    @Override
+    public boolean existsByThemeAndDateAndTime(Theme theme, LocalDate date, ReservationTime reservationTime) {
+        return reservations.stream()
+                .filter(reservation -> theme.equals(reservation.getTheme()))
+                .filter(reservation -> date.equals(reservation.getDate()))
+                .anyMatch(reservation -> reservationTime.equals(reservation.getReservationTime()));
+    }
+
+    @Override
+    public boolean existsByTime(ReservationTime reservationTime) {
+        return reservations.stream()
+                .anyMatch(reservation -> reservationTime.equals(reservation.getReservationTime()));
+    }
+
+    @Override
+    public boolean existsByTheme(Theme theme) {
+        return reservations.stream()
+                .anyMatch(reservation -> theme.equals(reservation.getTheme()));
+    }
+
     @Override
     public void delete(long id) {
         reservations.stream()
diff --git a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
index a22896c61f..1008c221f7 100644
--- a/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java
@@ -1,15 +1,20 @@
 package roomescape.repository;
 
+import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
+import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 public class CollectionReservationTimeRepository implements ReservationTimeRepository {
     private final List<ReservationTime> reservationTimes;
     private final AtomicLong atomicLong;
+    private final ReservationRepository reservationRepository;
+
 
     public CollectionReservationTimeRepository() {
         this(new ArrayList<>());
@@ -20,8 +25,18 @@ public CollectionReservationTimeRepository(List<ReservationTime> reservationTime
     }
 
     public CollectionReservationTimeRepository(List<ReservationTime> reservationTimes, AtomicLong atomicLong) {
+        this(reservationTimes, atomicLong, null);
+    }
+
+    public CollectionReservationTimeRepository(List<ReservationTime> reservationTimes, AtomicLong atomicLong,
+                                               ReservationRepository reservationRepository) {
         this.reservationTimes = reservationTimes;
         this.atomicLong = atomicLong;
+        this.reservationRepository = reservationRepository;
+    }
+
+    public CollectionReservationTimeRepository(ReservationRepository reservationRepository) {
+        this(new ArrayList<>(), new AtomicLong(), reservationRepository);
     }
 
     @Override
@@ -49,6 +64,18 @@ public List<ReservationTime> findAll() {
         return List.copyOf(reservationTimes);
     }
 
+    @Override
+    public List<ReservationTime> findUsedTimeByDateAndTheme(LocalDate date, Theme theme) {
+        if (reservationRepository == null) {
+            throw new UnsupportedOperationException("ReservationRepository 를 사용해 생성하지 않아 메서드를 사용할 수 없습니다.");
+        }
+        return reservationRepository.findAll().stream()
+                .filter(reservation -> date.equals(reservation.getDate()))
+                .filter(reservation -> theme.equals(reservation.getTheme()))
+                .map(Reservation::getReservationTime)
+                .toList();
+    }
+
     @Override
     public void delete(long id) {
         reservationTimes.stream()
diff --git a/src/test/java/roomescape/service/AvailableTimeServiceTest.java b/src/test/java/roomescape/service/AvailableTimeServiceTest.java
index 87b5427efd..24383b8577 100644
--- a/src/test/java/roomescape/service/AvailableTimeServiceTest.java
+++ b/src/test/java/roomescape/service/AvailableTimeServiceTest.java
@@ -14,20 +14,24 @@
 import roomescape.dto.AvailableTimeResponse;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.CollectionThemeRepository;
 import roomescape.repository.ReservationRepository;
 import roomescape.repository.ReservationTimeRepository;
+import roomescape.repository.ThemeRepository;
 
 class AvailableTimeServiceTest {
 
     private AvailableTimeService availableTimeService;
     private ReservationRepository reservationRepository;
     private ReservationTimeRepository reservationTimeRepository;
+    private ThemeRepository themeRepository;
 
     @BeforeEach
     void init() {
         reservationRepository = new CollectionReservationRepository();
-        reservationTimeRepository = new CollectionReservationTimeRepository();
-        availableTimeService = new AvailableTimeService(reservationRepository, reservationTimeRepository);
+        reservationTimeRepository = new CollectionReservationTimeRepository(reservationRepository);
+        themeRepository = new CollectionThemeRepository();
+        availableTimeService = new AvailableTimeService(reservationTimeRepository, themeRepository);
     }
 
     @DisplayName("날짜와 테마, 시간에 대한 예약 내역을 확인할 수 있다.")
@@ -35,6 +39,7 @@ void init() {
     void findAvailableTimeTest() {
         //given
         Theme DEFUALT_THEME = new Theme(1L, "name", "description", "thumbnail");
+        themeRepository.save(DEFUALT_THEME);
         ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(11, 0)));
         ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 0)));
         ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(13, 0)));

From a1ed68a191bf3435d283aa57bc00fa370fd72b57 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Mon, 6 May 2024 03:41:04 +0900
Subject: [PATCH 69/75] =?UTF-8?q?style:=20=EA=B8=B8=EC=9D=B4=EA=B0=80=20?=
 =?UTF-8?q?=EA=B8=B4=20SQL=20=EB=AC=B8=EC=9D=84=20=ED=85=8D=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=20=EB=B8=94=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=ED=91=9C?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcTemplateReservationRepository.java        | 11 ++++++-----
 .../JdbcTemplateReservationTimeRepository.java    |  3 ++-
 .../repository/JdbcTemplateThemeRepository.java   | 15 ++++++++++++++-
 3 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
index d6dffca4f7..c462b1f528 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java
@@ -46,7 +46,7 @@ private void save(Reservation reservation, KeyHolder keyHolder) {
     @Override
     public List<Reservation> findAll() {
         String query = """
-                   SELECT 
+                SELECT
                    r.id AS reservation_id,
                    r.name AS reservation_name,
                    r.date AS reservation_date,
@@ -57,10 +57,11 @@ public List<Reservation> findAll() {
                    t2.description AS description,
                    t2.thumbnail AS thumbnail
                 FROM reservation AS r
-                INNER JOIN reservation_time t 
-                ON r.time_id = t.id
-                INNER JOIN theme t2  
-                ON t2.id = r.theme_id""";
+                INNER JOIN reservation_time t
+                    ON r.time_id = t.id
+                INNER JOIN theme t2
+                    ON t2.id = r.theme_id
+                """;
         return jdbcTemplate.query(query, reservationRowMapper);
     }
 
diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
index da84250a1b..3e001eb47d 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java
@@ -65,7 +65,8 @@ public List<ReservationTime> findAll() {
     @Override
     public List<ReservationTime> findUsedTimeByDateAndTheme(LocalDate date, Theme theme) {
         String sql = """
-                SELECT rt.id, start_at
+                SELECT
+                    rt.id, start_at
                 FROM reservation_time rt
                 JOIN reservation r
                     ON rt.id = r.time_id
diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
index dd95c751cb..c9871e1eda 100644
--- a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
+++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java
@@ -29,7 +29,20 @@ public List<Theme> findAll() {
 
     @Override
     public List<Theme> findAndOrderByPopularity(LocalDate start, LocalDate end, int count) {
-        String sql = "SELECT th.id AS id, th.name AS name, description, thumbnail, COUNT(*) AS count FROM theme th JOIN reservation r ON r.theme_id = th.id WHERE PARSEDATETIME(r.date,'yyyy-MM-dd') >= PARSEDATETIME(?,'yyyy-MM-dd') AND PARSEDATETIME(r.date,'yyyy-MM-dd') <= PARSEDATETIME(?,'yyyy-MM-dd') GROUP BY th.id ORDER BY count DESC LIMIT ?";
+        String sql = """
+                SELECT
+                    th.id AS id, th.name AS name, description, thumbnail, COUNT(*) AS count
+                FROM theme th
+                JOIN reservation r
+                    ON r.theme_id = th.id
+                WHERE
+                    PARSEDATETIME(r.date,'yyyy-MM-dd') >= PARSEDATETIME(?,'yyyy-MM-dd')
+                AND
+                    PARSEDATETIME(r.date,'yyyy-MM-dd') <= PARSEDATETIME(?,'yyyy-MM-dd')
+                GROUP BY th.id
+                ORDER BY count DESC
+                LIMIT ?
+                """;
         return jdbcTemplate.query(sql, themeRowMapper, start, end, count);
     }
 

From 5d02747f5b9b1e61e4cd117f8e91e4fd54c7a0f6 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 7 May 2024 12:03:37 +0900
Subject: [PATCH 70/75] =?UTF-8?q?test:=20=EC=9D=B8=EA=B8=B0=20=ED=85=8C?=
 =?UTF-8?q?=EB=A7=88=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/CollectionThemeRepository.java | 42 +++++++++++++++--
 .../roomescape/service/ThemeServiceTest.java  | 46 +++++++++++++++++--
 2 files changed, 82 insertions(+), 6 deletions(-)

diff --git a/src/test/java/roomescape/repository/CollectionThemeRepository.java b/src/test/java/roomescape/repository/CollectionThemeRepository.java
index 2774877fc3..98ba2856f4 100644
--- a/src/test/java/roomescape/repository/CollectionThemeRepository.java
+++ b/src/test/java/roomescape/repository/CollectionThemeRepository.java
@@ -4,18 +4,32 @@
 import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+import roomescape.domain.Reservation;
 import roomescape.domain.Theme;
 
 public class CollectionThemeRepository implements ThemeRepository {
 
     private final List<Theme> themes;
     private final AtomicLong index;
+    private final CollectionReservationRepository reservationRepository;
 
     public CollectionThemeRepository() {
-        themes = new ArrayList<>();
-        index = new AtomicLong(0);
+        this(new ArrayList<>(), new AtomicLong(0), null);
+    }
+
+    private CollectionThemeRepository(List<Theme> themes, AtomicLong index,
+                                      CollectionReservationRepository reservationRepository) {
+        this.themes = themes;
+        this.index = index;
+        this.reservationRepository = reservationRepository;
+    }
+
+    public CollectionThemeRepository(CollectionReservationRepository reservationRepository) {
+        this(new ArrayList<>(), new AtomicLong(0), reservationRepository);
     }
 
     @Override
@@ -25,7 +39,29 @@ public List<Theme> findAll() {
 
     @Override
     public List<Theme> findAndOrderByPopularity(LocalDate start, LocalDate end, int count) {
-        return null;
+        if (reservationRepository == null) {
+            throw new UnsupportedOperationException("ReservationRepository 를 사용해 생성하지 않아 메서드를 사용할 수 없습니다.");
+        }
+        Map<Long, Integer> collect = reservationRepository.findAll().stream()
+                .filter(reservation -> isAfterStart(start, reservation))
+                .filter(reservation -> isBeforeEnd(end, reservation))
+                .map(Reservation::getTheme)
+                .collect(Collectors.groupingBy(Theme::getId, Collectors.summingInt(value -> 1)));
+        return collect.keySet()
+                .stream()
+                .sorted((o1, o2) -> Integer.compare(collect.get(o2), collect.get(o1)))
+                .map(id -> findById(id).orElseThrow())
+                .toList();
+    }
+
+    private boolean isAfterStart(LocalDate start, Reservation reservation) {
+        LocalDate date = reservation.getDate();
+        return date.isAfter(start) || date.isEqual(start);
+    }
+
+    private boolean isBeforeEnd(LocalDate end, Reservation reservation) {
+        LocalDate date = reservation.getDate();
+        return date.isBefore(end) || date.isEqual(end);
     }
 
     @Override
diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java
index 8e1505d457..8605a8ed0e 100644
--- a/src/test/java/roomescape/service/ThemeServiceTest.java
+++ b/src/test/java/roomescape/service/ThemeServiceTest.java
@@ -9,6 +9,7 @@
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
+import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -16,27 +17,27 @@
 import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
 import roomescape.domain.Theme;
+import roomescape.dto.ReservationRequest;
 import roomescape.dto.ThemeRequest;
 import roomescape.dto.ThemeResponse;
 import roomescape.exception.RoomescapeException;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
 import roomescape.repository.CollectionThemeRepository;
-import roomescape.repository.ReservationRepository;
 import roomescape.repository.ThemeRepository;
 
 class ThemeServiceTest {
 
     private ThemeRepository themeRepository;
     private CollectionReservationTimeRepository reservationTimeRepository;
-    private ReservationRepository reservationRepository;
+    private CollectionReservationRepository reservationRepository;
     private ThemeService themeService;
 
     @BeforeEach
     void initService() {
-        themeRepository = new CollectionThemeRepository();
         reservationTimeRepository = new CollectionReservationTimeRepository();
         reservationRepository = new CollectionReservationRepository();
+        themeRepository = new CollectionThemeRepository();
         themeService = new ThemeService(themeRepository, reservationRepository);
     }
 
@@ -56,6 +57,45 @@ void findAllTest() {
         assertThat(themeResponses).hasSize(4);
     }
 
+    @DisplayName("인기 테마를 조회할 수 있다.")
+    @Test
+    void findAndOrderByPopularity() {
+        themeRepository = new CollectionThemeRepository(reservationRepository);
+        themeService = new ThemeService(themeRepository, reservationRepository);
+        LocalDate date = LocalDate.now().plusDays(1);
+
+        addReservations(date);
+
+        LocalDate end = date.plusDays(6);
+        List<Long> themeIds = themeService.findAndOrderByPopularity(date, end, 10)
+                .stream()
+                .map(ThemeResponse::id)
+                .toList();
+        Assertions.assertThat(themeIds)
+                .containsExactly(2L, 1L, 3L);
+    }
+
+    private void addReservations(LocalDate date) {
+        Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
+        Theme theme2 = themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
+        Theme theme3 = themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
+        ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(1, 30)));
+        ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(2, 30)));
+        ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(3, 30)));
+
+        ReservationService reservationService = new ReservationService(reservationRepository, reservationTimeRepository,
+                themeRepository);
+
+        reservationService.save(new ReservationRequest(date, "name", reservationTime2.getId(), theme2.getId()));
+        reservationService.save(new ReservationRequest(date, "name", reservationTime1.getId(), theme2.getId()));
+        reservationService.save(new ReservationRequest(date, "name", reservationTime3.getId(), theme2.getId()));
+
+        reservationService.save(new ReservationRequest(date, "name", reservationTime1.getId(), theme1.getId()));
+        reservationService.save(new ReservationRequest(date, "name", reservationTime2.getId(), theme1.getId()));
+
+        reservationService.save(new ReservationRequest(date, "name", reservationTime1.getId(), theme3.getId()));
+    }
+
     @DisplayName("테마, 시간이 하나 존재할 때")
     @Nested
     class OneThemeTest {

From eb38c7da9921bebd5b0ea30e1bf97dc159c073c1 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 7 May 2024 12:50:50 +0900
Subject: [PATCH 71/75] =?UTF-8?q?test:=20=EB=88=84=EB=9D=BD=EB=90=9C=20Rep?=
 =?UTF-8?q?ository=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ...JdbcTemplateReservationRepositoryTest.java | 64 ++++++++++++--
 ...TemplateReservationTimeRepositoryTest.java | 47 +++++++++-
 .../JdbcTemplateThemeRepositoryTest.java      | 85 +++++++++++++++++++
 3 files changed, 184 insertions(+), 12 deletions(-)
 create mode 100644 src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java

diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
index 79dfd24631..54dcd71f38 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
@@ -1,5 +1,7 @@
 package roomescape.repository;
 
+import static org.junit.jupiter.api.Assertions.assertAll;
+
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
@@ -26,17 +28,17 @@ class JdbcTemplateReservationRepositoryTest {
 
     @BeforeEach
     void init() {
-        jdbcTemplate.update("delete from reservation");
-        jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
+        jdbcTemplate.update("DELETE FROM reservation");
+        jdbcTemplate.update("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1");
 
-        jdbcTemplate.update("delete from reservation_time");
-        jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
-        jdbcTemplate.update("insert into reservation_time(start_at) values('11:56')");
+        jdbcTemplate.update("DELETE FROM reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("INSERT INTO reservation_time(start_at) VALUES('11:56')");
 
-        jdbcTemplate.update("delete from theme");
-        jdbcTemplate.update("ALTER TABLE theme alter column id restart with 1");
+        jdbcTemplate.update("DELETE FROM theme");
+        jdbcTemplate.update("ALTER TABLE theme ALTER COLUMN id RESTART WITH 1");
         jdbcTemplate.update(
-                "insert into theme (name, description, thumbnail) values('name', 'description', 'thumbnail')");
+                "INSERT INTO theme (name, description, thumbnail) VALUES('name', 'description', 'thumbnail')");
 
     }
 
@@ -78,4 +80,50 @@ void delete() {
         Assertions.assertThat(beforeSaveAndDelete)
                 .containsExactlyElementsOf(afterSaveAndDelete);
     }
+
+    @Test
+    @DisplayName("특정 테마에 특정 날짜 특정 시간에 예약 여부를 잘 반환하는지 확인한다.")
+    void existsByThemeAndDateAndTime() {
+        LocalDate date1 = LocalDate.now();
+        LocalDate date2 = date1.plusDays(1);
+        reservationRepository.save(new Reservation("name", date1, DEFAULT_TIME, DEFAULT_THEME));
+
+        assertAll(
+                () -> Assertions.assertThat(
+                                reservationRepository.existsByThemeAndDateAndTime(DEFAULT_THEME, date1, DEFAULT_TIME))
+                        .isTrue(),
+                () -> Assertions.assertThat(
+                                reservationRepository.existsByThemeAndDateAndTime(DEFAULT_THEME, date2, DEFAULT_TIME))
+                        .isFalse()
+        );
+    }
+
+    @Test
+    @DisplayName("특정 시간에 예약이 있는지 확인한다.")
+    void existsByTime() {
+        LocalDate date = LocalDate.now();
+        reservationRepository.save(new Reservation("name", date, DEFAULT_TIME, DEFAULT_THEME));
+
+        assertAll(
+                () -> Assertions.assertThat(reservationRepository.existsByTime(DEFAULT_TIME))
+                        .isTrue(),
+                () -> Assertions.assertThat(
+                                reservationRepository.existsByTime(new ReservationTime(2L, LocalTime.of(12, 56))))
+                        .isFalse()
+        );
+    }
+
+    @Test
+    @DisplayName("특정 테마에 예약이 있는지 확인한다.")
+    void existsByTheme() {
+        LocalDate date = LocalDate.now();
+        reservationRepository.save(new Reservation("name", date, DEFAULT_TIME, DEFAULT_THEME));
+
+        assertAll(
+                () -> Assertions.assertThat(reservationRepository.existsByTheme(DEFAULT_THEME))
+                        .isTrue(),
+                () -> Assertions.assertThat(reservationRepository.existsByTheme(new Theme(2L, DEFAULT_THEME)))
+                        .isFalse()
+        );
+    }
 }
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
index 71d364044d..8c29259eb2 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java
@@ -1,5 +1,8 @@
 package roomescape.repository;
 
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.List;
 import org.assertj.core.api.Assertions;
@@ -9,21 +12,27 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.jdbc.core.JdbcTemplate;
+import roomescape.domain.Reservation;
 import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
 
 @SpringBootTest
 class JdbcTemplateReservationTimeRepositoryTest {
     @Autowired
     private ReservationTimeRepository reservationTimeRepository;
     @Autowired
+    private ReservationRepository reservationRepository;
+    @Autowired
+    private ThemeRepository themeRepository;
+    @Autowired
     private JdbcTemplate jdbcTemplate;
 
     @BeforeEach
     void init() {
-        jdbcTemplate.update("delete from reservation");
-        jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
-        jdbcTemplate.update("delete from reservation_time");
-        jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
+        jdbcTemplate.update("DELETE FROM reservation");
+        jdbcTemplate.update("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("DELETE FROM reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time ALTER COLUMN id RESTART WITH 1");
     }
 
     @Test
@@ -64,4 +73,34 @@ void delete() {
         Assertions.assertThat(beforeSaveAndDelete)
                 .containsExactlyInAnyOrderElementsOf(afterSaveAndDelete);
     }
+
+    @Test
+    @DisplayName("특정 시작 시간을 가지는 예약 시간이 있는지 여부를 잘 반환하는지 확인한다.")
+    void existsByStartAt() {
+        LocalTime time = LocalTime.of(11, 20);
+        reservationTimeRepository.save(new ReservationTime(time));
+
+        assertAll(
+                () -> Assertions.assertThat(reservationTimeRepository.existsByStartAt(time))
+                        .isTrue(),
+                () -> Assertions.assertThat(reservationTimeRepository.existsByStartAt(time.plusHours(1)))
+                        .isFalse()
+        );
+    }
+
+    @Test
+    @DisplayName("특정 날짜와 테마에 예약이 있는 예약 시간의 목록을 잘 반환하는지 확인한다.")
+    void findUsedTimeByDateAndTheme() {
+        LocalDate date = LocalDate.of(2024, 12, 30);
+        Theme theme = new Theme("name", "description", "http://example.com");
+        theme = themeRepository.save(theme);
+        ReservationTime time = new ReservationTime(LocalTime.of(12, 30));
+        time = reservationTimeRepository.save(time);
+        reservationRepository.save(new Reservation("name", date, time, theme));
+
+        List<ReservationTime> response = reservationTimeRepository.findUsedTimeByDateAndTheme(date, theme);
+
+        Assertions.assertThat(response)
+                .containsExactly(time);
+    }
 }
diff --git a/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java
new file mode 100644
index 0000000000..5f10e6cda7
--- /dev/null
+++ b/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java
@@ -0,0 +1,85 @@
+package roomescape.repository;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+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 roomescape.domain.Reservation;
+import roomescape.domain.ReservationTime;
+import roomescape.domain.Theme;
+
+@SpringBootTest
+class JdbcTemplateThemeRepositoryTest {
+
+    @Autowired
+    private ThemeRepository themeRepository;
+    @Autowired
+    private ReservationRepository reservationRepository;
+    @Autowired
+    private ReservationTimeRepository reservationTimeRepository;
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @BeforeEach
+    void init() {
+        jdbcTemplate.update("DELETE FROM reservation");
+        jdbcTemplate.update("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("DELETE FROM reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("DELETE FROM theme");
+        jdbcTemplate.update("ALTER TABLE theme ALTER COLUMN id RESTART WITH 1");
+    }
+
+    @Test
+    @DisplayName("전체 테마 조회를 잘 하는지 확인")
+    void findAll() {
+        Theme theme = new Theme("name", "description", "http://example.com");
+        theme = themeRepository.save(theme);
+        List<Theme> allTheme = themeRepository.findAll();
+
+        Assertions.assertThat(allTheme)
+                .containsExactly(theme);
+    }
+
+    @Test
+    void findAndOrderByPopularity() {
+        Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
+        Theme theme2 = themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
+        Theme theme3 = themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
+
+        ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(1, 30)));
+        ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(2, 30)));
+        ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(3, 30)));
+
+        LocalDate date = LocalDate.now().plusDays(1);
+        reservationRepository.save(new Reservation("name", date, reservationTime2, theme2));
+        reservationRepository.save(new Reservation("name", date, reservationTime1, theme2));
+        reservationRepository.save(new Reservation("name", date, reservationTime3, theme2));
+
+        reservationRepository.save(new Reservation("name", date, reservationTime1, theme1));
+        reservationRepository.save(new Reservation("name", date, reservationTime2, theme1));
+
+        reservationRepository.save(new Reservation("name", date, reservationTime1, theme3));
+
+        List<Theme> result = themeRepository.findAndOrderByPopularity(date, date.plusDays(1), 10);
+        Assertions.assertThat(result)
+                .containsExactly(theme2, theme1, theme3);
+    }
+
+    @Test
+    @DisplayName("테마가 잘 지워지는지 확인")
+    void delete() {
+        Theme theme = themeRepository.save(new Theme("name1", "description1", "thumbnail"));
+
+        themeRepository.delete(theme.getId());
+
+        Assertions.assertThat(themeRepository.findAll())
+                .isEmpty();
+    }
+}

From 9881fc7cb95394dad3b79728d6dba66c4590e2f2 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 7 May 2024 12:54:01 +0900
Subject: [PATCH 72/75] =?UTF-8?q?docs:=20=EB=A6=AC=ED=8C=A9=ED=86=A0?=
 =?UTF-8?q?=EB=A7=81=20=EB=AA=A9=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 REFACTORING_LIST.md | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/REFACTORING_LIST.md b/REFACTORING_LIST.md
index 62a7673edf..2882536e81 100644
--- a/REFACTORING_LIST.md
+++ b/REFACTORING_LIST.md
@@ -4,9 +4,4 @@
 
 1. Repository.findAll() + Stream 을 사용한 코드
 2. SQL 표준 작성 방식에 따라 대소문자 통일
-3. Repository 의 인메모리 구현체 대신 다른 종류의 테스트 더블 사용하기
-
-### 우선순위 낮음
-
-1. Service의 일부 검증 로직을 도메인으로 이동
-    - 조회 도메인과 생성 도메인을 분리
+3. ~~Repository 의 인메모리 구현체 대신 다른 종류의 테스트 더블 사용하기~~

From 239c24149981fcc37076a19db38019927bd510b5 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 7 May 2024 13:28:28 +0900
Subject: [PATCH 73/75] =?UTF-8?q?feat:=20=ED=85=8C=EB=A7=88=EC=9D=98=20thu?=
 =?UTF-8?q?mbnail=20=EC=9D=B4=20url=20=ED=98=95=ED=83=9C=EC=9D=B8=EC=A7=80?=
 =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/roomescape/domain/Theme.java    |  5 ++++
 .../roomescape/exception/ExceptionType.java   |  1 +
 .../controller/ReservationControllerTest.java |  2 +-
 .../roomescape/domain/ReservationTest.java    |  2 +-
 .../java/roomescape/domain/ThemeTest.java     | 26 +++++++++++--------
 .../integration/AdminIntegrationTest.java     | 22 ++++++++--------
 ...JdbcTemplateReservationRepositoryTest.java |  4 +--
 .../JdbcTemplateThemeRepositoryTest.java      |  8 +++---
 .../service/AvailableTimeServiceTest.java     |  2 +-
 .../service/ReservationServiceTest.java       |  2 +-
 .../service/ReservationTimeServiceTest.java   |  2 +-
 .../roomescape/service/ThemeServiceTest.java  | 20 +++++++-------
 12 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java
index 83f72ce50b..ac1d854e09 100644
--- a/src/main/java/roomescape/domain/Theme.java
+++ b/src/main/java/roomescape/domain/Theme.java
@@ -3,6 +3,7 @@
 import static roomescape.exception.ExceptionType.EMPTY_DESCRIPTION;
 import static roomescape.exception.ExceptionType.EMPTY_NAME;
 import static roomescape.exception.ExceptionType.EMPTY_THUMBNAIL;
+import static roomescape.exception.ExceptionType.NOT_URL_BASE_THUMBNAIL;
 
 import java.util.Objects;
 import roomescape.exception.RoomescapeException;
@@ -43,6 +44,10 @@ private void validateThumbnail(String thumbnail) {
         if (thumbnail == null || thumbnail.isBlank()) {
             throw new RoomescapeException(EMPTY_THUMBNAIL);
         }
+
+        if (!thumbnail.startsWith("http://") && !thumbnail.startsWith("https://")) {
+            throw new RoomescapeException(NOT_URL_BASE_THUMBNAIL);
+        }
     }
 
     public Theme(String name, String description, String thumbnail) {
diff --git a/src/main/java/roomescape/exception/ExceptionType.java b/src/main/java/roomescape/exception/ExceptionType.java
index e0d16e572c..7f71d71bcf 100644
--- a/src/main/java/roomescape/exception/ExceptionType.java
+++ b/src/main/java/roomescape/exception/ExceptionType.java
@@ -11,6 +11,7 @@ public enum ExceptionType {
     EMPTY_THEME(BAD_REQUEST, "테마는 필수값 입니다."),
     EMPTY_DESCRIPTION(BAD_REQUEST, "테마 설명은 필수값 입니다."),
     EMPTY_THUMBNAIL(BAD_REQUEST, "테마 썸네일은 필수값 입니다."),
+    NOT_URL_BASE_THUMBNAIL(BAD_REQUEST, "테마 썸네일이 url 형태가 아닙니다."),
     PAST_TIME_RESERVATION(BAD_REQUEST, "이미 지난 시간에 예약할 수 없습니다."),
     DUPLICATE_RESERVATION(BAD_REQUEST, "같은 시간에 이미 예약이 존재합니다."),
     DUPLICATE_RESERVATION_TIME(BAD_REQUEST, "이미 예약시간이 존재합니다."),
diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java
index 277eedaba8..60a44b2504 100644
--- a/src/test/java/roomescape/controller/ReservationControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationControllerTest.java
@@ -25,7 +25,7 @@ class ReservationControllerTest {
     private static final long TIME_ID = 1L;
     private static final LocalTime TIME = LocalTime.now();
     private ReservationTime defaultTime = new ReservationTime(TIME_ID, TIME);
-    private Theme defualtTheme = new Theme("name", "description", "thumbnail");
+    private Theme defualtTheme = new Theme("name", "description", "http://thumbnail");
 
     private CollectionReservationRepository collectionReservationRepository;
     private ReservationController reservationController;
diff --git a/src/test/java/roomescape/domain/ReservationTest.java b/src/test/java/roomescape/domain/ReservationTest.java
index 6c20ebe0a0..1abdae0c0b 100644
--- a/src/test/java/roomescape/domain/ReservationTest.java
+++ b/src/test/java/roomescape/domain/ReservationTest.java
@@ -18,7 +18,7 @@ class ReservationTest {
 
     private static final LocalDate DEFAULT_DATE = LocalDate.now();
     private static final ReservationTime DEFAULT_TIME = new ReservationTime(1L, LocalTime.now());
-    private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "썸네일");
+    private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "http://썸네일");
 
     @DisplayName("생성 테스트")
     @Test
diff --git a/src/test/java/roomescape/domain/ThemeTest.java b/src/test/java/roomescape/domain/ThemeTest.java
index a5b3e088d4..c2b359ae87 100644
--- a/src/test/java/roomescape/domain/ThemeTest.java
+++ b/src/test/java/roomescape/domain/ThemeTest.java
@@ -7,6 +7,7 @@
 import static roomescape.exception.ExceptionType.EMPTY_DESCRIPTION;
 import static roomescape.exception.ExceptionType.EMPTY_NAME;
 import static roomescape.exception.ExceptionType.EMPTY_THUMBNAIL;
+import static roomescape.exception.ExceptionType.NOT_URL_BASE_THUMBNAIL;
 
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -18,25 +19,28 @@ class ThemeTest {
     @Test
     void constructTest() {
         assertAll(
-                () -> assertThatThrownBy(() -> new Theme(null, "description", "thumbnail"))
+                () -> assertThatThrownBy(() -> new Theme(null, "description", "http://thumbnail"))
                         .isInstanceOf(RoomescapeException.class)
                         .hasMessage(EMPTY_NAME.getMessage()),
 
-                () -> assertThatThrownBy(() -> new Theme("name", null, "thumbnail"))
+                () -> assertThatThrownBy(() -> new Theme("name", null, "http://thumbnail"))
                         .isInstanceOf(RoomescapeException.class)
                         .hasMessage(EMPTY_DESCRIPTION.getMessage()),
 
                 () -> assertThatThrownBy(() -> new Theme("name", "description", null))
                         .isInstanceOf(RoomescapeException.class)
                         .hasMessage(EMPTY_THUMBNAIL.getMessage()),
+                () -> assertThatThrownBy(() -> new Theme("name", "description", "not url"))
+                        .isInstanceOf(RoomescapeException.class)
+                        .hasMessage(NOT_URL_BASE_THUMBNAIL.getMessage()),
 
-                () -> assertThatCode(() -> new Theme("name", "description", "thumbnail"))
+                () -> assertThatCode(() -> new Theme("name", "description", "http://thumbnail"))
                         .doesNotThrowAnyException(),
 
-                () -> assertThatCode(() -> new Theme(null, "name", "description", "thumbnail"))
+                () -> assertThatCode(() -> new Theme(null, "name", "description", "http://thumbnail"))
                         .doesNotThrowAnyException(),
 
-                () -> assertThatCode(() -> new Theme(1L, "name", "description", "thumbnail"))
+                () -> assertThatCode(() -> new Theme(1L, "name", "description", "http://thumbnail"))
                         .doesNotThrowAnyException()
         );
     }
@@ -45,14 +49,14 @@ void constructTest() {
     @Test
     void equalsTest() {
         assertAll(
-                () -> assertThat(new Theme(1L, "name", "description", "thumbnail"))
-                        .isEqualTo(new Theme(1L, "otherName", "otherDescription", "otherThumbnail")),
+                () -> assertThat(new Theme(1L, "name", "description", "http://thumbnail"))
+                        .isEqualTo(new Theme(1L, "otherName", "otherDescription", "http://otherThumbnail")),
 
-                () -> assertThat(new Theme(1L, "sameName", "sameDescription", "sameThumbnail"))
-                        .isNotEqualTo(new Theme(2L, "sameName", "sameDescription", "sameThumbnail")),
+                () -> assertThat(new Theme(1L, "sameName", "sameDescription", "http://sameThumbnail"))
+                        .isNotEqualTo(new Theme(2L, "sameName", "sameDescription", "http://sameThumbnail")),
 
-                () -> assertThat(new Theme(1L, "sameName", "sameDescription", "sameThumbnail"))
-                        .isNotEqualTo(new Theme(null, "sameName", "sameDescription", "sameThumbnail"))
+                () -> assertThat(new Theme(1L, "sameName", "sameDescription", "http://sameThumbnail"))
+                        .isNotEqualTo(new Theme(null, "sameName", "sameDescription", "http://sameThumbnail"))
         );
     }
 }
diff --git a/src/test/java/roomescape/integration/AdminIntegrationTest.java b/src/test/java/roomescape/integration/AdminIntegrationTest.java
index 2db65f8542..73d9bffb46 100644
--- a/src/test/java/roomescape/integration/AdminIntegrationTest.java
+++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java
@@ -28,14 +28,14 @@ public class AdminIntegrationTest {
 
     @BeforeEach
     void init() {
-        jdbcTemplate.update("delete from reservation");
-        jdbcTemplate.update("ALTER TABLE reservation alter column id restart with 1");
-        jdbcTemplate.update("delete from reservation_time");
-        jdbcTemplate.update("ALTER TABLE reservation_time alter column id restart with 1");
-        jdbcTemplate.update("insert into reservation_time(start_at) values('11:56')");
-        jdbcTemplate.update("delete from THEME");
-        jdbcTemplate.update("ALTER TABLE THEME alter column id restart with 1");
-        jdbcTemplate.update("insert into THEME values ( 1,'a','a','a')");
+        jdbcTemplate.update("DELETE FROM reservation");
+        jdbcTemplate.update("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("DELETE FROM reservation_time");
+        jdbcTemplate.update("ALTER TABLE reservation_time ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("INSERT INTO reservation_time(start_at) VALUES('11:56')");
+        jdbcTemplate.update("DELETE FROM theme");
+        jdbcTemplate.update("ALTER TABLE theme ALTER COLUMN id RESTART WITH 1");
+        jdbcTemplate.update("INSERT INTO theme VALUES ( 1,'name','description','http://thumbnail')");
         RestAssured.port = port;
     }
 
@@ -66,7 +66,7 @@ void adminReservationPageLoad() {
     @Test
     @DisplayName("관리자 예약 페이지가 잘 동작한다.")
     void adminReservationPageWork() {
-        Integer integer = jdbcTemplate.queryForObject("select id from reservation_time",
+        Integer integer = jdbcTemplate.queryForObject("SELECT id FROM reservation_time",
                 Integer.class);
         System.out.println(integer);
         Map<String, Object> params = new HashMap<>();
@@ -117,7 +117,7 @@ void adminReservationPageWorkWithDB() {
                 .then().log().all()
                 .statusCode(201);
 
-        Integer count = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
+        Integer count = jdbcTemplate.queryForObject("SELECT COUNT(1) FROM reservation", Integer.class);
         Assertions.assertThat(count).isEqualTo(1);
 
         RestAssured.given().log().all()
@@ -125,7 +125,7 @@ void adminReservationPageWorkWithDB() {
                 .then().log().all()
                 .statusCode(204);
 
-        Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT count(1) from reservation", Integer.class);
+        Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT COUNT(1) FROM reservation", Integer.class);
         Assertions.assertThat(countAfterDelete).isEqualTo(0);
     }
 
diff --git a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
index 54dcd71f38..f51d126f1b 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java
@@ -19,7 +19,7 @@
 @SpringBootTest
 class JdbcTemplateReservationRepositoryTest {
     private static final ReservationTime DEFAULT_TIME = new ReservationTime(1L, LocalTime.of(11, 56));
-    private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "썸네일");
+    private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "http://썸네일");
 
     @Autowired
     private ReservationRepository reservationRepository;
@@ -38,7 +38,7 @@ void init() {
         jdbcTemplate.update("DELETE FROM theme");
         jdbcTemplate.update("ALTER TABLE theme ALTER COLUMN id RESTART WITH 1");
         jdbcTemplate.update(
-                "INSERT INTO theme (name, description, thumbnail) VALUES('name', 'description', 'thumbnail')");
+                "INSERT INTO theme (name, description, thumbnail) VALUES('name', 'description', 'http://thumbnail')");
 
     }
 
diff --git a/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java
index 5f10e6cda7..60ea789407 100644
--- a/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java
+++ b/src/test/java/roomescape/repository/JdbcTemplateThemeRepositoryTest.java
@@ -49,9 +49,9 @@ void findAll() {
 
     @Test
     void findAndOrderByPopularity() {
-        Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
-        Theme theme2 = themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
-        Theme theme3 = themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
+        Theme theme1 = themeRepository.save(new Theme("name1", "description1", "http://thumbnail1"));
+        Theme theme2 = themeRepository.save(new Theme("name2", "description2", "http://thumbnail2"));
+        Theme theme3 = themeRepository.save(new Theme("name3", "description3", "http://thumbnail3"));
 
         ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(1, 30)));
         ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(2, 30)));
@@ -75,7 +75,7 @@ void findAndOrderByPopularity() {
     @Test
     @DisplayName("테마가 잘 지워지는지 확인")
     void delete() {
-        Theme theme = themeRepository.save(new Theme("name1", "description1", "thumbnail"));
+        Theme theme = themeRepository.save(new Theme("name1", "description1", "http://thumbnail"));
 
         themeRepository.delete(theme.getId());
 
diff --git a/src/test/java/roomescape/service/AvailableTimeServiceTest.java b/src/test/java/roomescape/service/AvailableTimeServiceTest.java
index 24383b8577..cc953821c4 100644
--- a/src/test/java/roomescape/service/AvailableTimeServiceTest.java
+++ b/src/test/java/roomescape/service/AvailableTimeServiceTest.java
@@ -38,7 +38,7 @@ void init() {
     @Test
     void findAvailableTimeTest() {
         //given
-        Theme DEFUALT_THEME = new Theme(1L, "name", "description", "thumbnail");
+        Theme DEFUALT_THEME = new Theme(1L, "name", "description", "http://thumbnail");
         themeRepository.save(DEFUALT_THEME);
         ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(11, 0)));
         ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 0)));
diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java
index b28d9bced8..7780d186e3 100644
--- a/src/test/java/roomescape/service/ReservationServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationServiceTest.java
@@ -35,7 +35,7 @@ class ReservationServiceTest {
     private ReservationService reservationService;
 
     private ReservationTime defaultTime = new ReservationTime(LocalTime.now());
-    private Theme defaultTheme = new Theme("name", "description", "thumbnail");
+    private Theme defaultTheme = new Theme("name", "description", "http://thumbnail");
 
     @BeforeEach
     void initService() {
diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
index 0b0cd90642..38c6c7f1bf 100644
--- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java
+++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java
@@ -98,7 +98,7 @@ void usedReservationTimeDeleteTest() {
                     "name",
                     LocalDate.now(),
                     new ReservationTime(1L, SAVED_TIME),
-                    new Theme(1L, "name", "description", "thumbnail")
+                    new Theme(1L, "name", "description", "http://thumbnail")
             ));
 
             //when & then
diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java
index 8605a8ed0e..5ea3366dbf 100644
--- a/src/test/java/roomescape/service/ThemeServiceTest.java
+++ b/src/test/java/roomescape/service/ThemeServiceTest.java
@@ -45,10 +45,10 @@ void initService() {
     @Test
     void findAllTest() {
         //given
-        themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
-        themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
-        themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
-        themeRepository.save(new Theme("name4", "description4", "thumbnail4"));
+        themeRepository.save(new Theme("name1", "description1", "http://thumbnail1"));
+        themeRepository.save(new Theme("name2", "description2", "http://thumbnail2"));
+        themeRepository.save(new Theme("name3", "description3", "http://thumbnail3"));
+        themeRepository.save(new Theme("name4", "description4", "http://thumbnail4"));
 
         //when
         List<ThemeResponse> themeResponses = themeService.findAll();
@@ -76,9 +76,9 @@ void findAndOrderByPopularity() {
     }
 
     private void addReservations(LocalDate date) {
-        Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
-        Theme theme2 = themeRepository.save(new Theme("name2", "description2", "thumbnail2"));
-        Theme theme3 = themeRepository.save(new Theme("name3", "description3", "thumbnail3"));
+        Theme theme1 = themeRepository.save(new Theme("name1", "description1", "http://thumbnail1"));
+        Theme theme2 = themeRepository.save(new Theme("name2", "description2", "http://thumbnail2"));
+        Theme theme3 = themeRepository.save(new Theme("name3", "description3", "http://thumbnail3"));
         ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(1, 30)));
         ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(2, 30)));
         ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(3, 30)));
@@ -100,7 +100,7 @@ private void addReservations(LocalDate date) {
     @Nested
     class OneThemeTest {
         private ReservationTime defaultTime = new ReservationTime(LocalTime.now().plusMinutes(5));
-        private Theme defaultTheme = new Theme("name", "description", "thumbnail");
+        private Theme defaultTheme = new Theme("name", "description", "http://thumbnail");
 
         @BeforeEach
         void addDefaultData() {
@@ -112,7 +112,7 @@ void addDefaultData() {
         @Test
         void duplicatedThemeSaveFailTest() {
             assertThatThrownBy(() -> themeService.save(new ThemeRequest(
-                    defaultTheme.getName(), "description", "thumbnail"
+                    defaultTheme.getName(), "description", "http://thumbnail"
             ))).isInstanceOf(RoomescapeException.class)
                     .hasMessage(DUPLICATE_THEME.getMessage());
         }
@@ -120,7 +120,7 @@ void duplicatedThemeSaveFailTest() {
         @DisplayName("다른 이름의 테마를 예약할 수 있다.")
         @Test
         void notDuplicatedThemeNameSaveTest() {
-            themeService.save(new ThemeRequest("otherName", "description", "thumbnail"));
+            themeService.save(new ThemeRequest("otherName", "description", "http://thumbnail"));
 
             assertThat(themeRepository.findAll())
                     .hasSize(2);

From 20fdddef794de6a7aa1ba05f1b9f0c9c37c08188 Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 7 May 2024 13:39:39 +0900
Subject: [PATCH 74/75] =?UTF-8?q?refactor:=20=EC=A0=95=EC=A0=81=20?=
 =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20=EB=B0=98=ED=99=98?=
 =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?=
 =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../{AdminController.java => AdminPageController.java}    | 2 +-
 .../{UserController.java => UserPageController.java}      | 2 +-
 ...inControllerTest.java => AdminPageControllerTest.java} | 8 ++++----
 3 files changed, 6 insertions(+), 6 deletions(-)
 rename src/main/java/roomescape/controller/{AdminController.java => AdminPageController.java} (94%)
 rename src/main/java/roomescape/controller/{UserController.java => UserPageController.java} (91%)
 rename src/test/java/roomescape/controller/{AdminControllerTest.java => AdminPageControllerTest.java} (80%)

diff --git a/src/main/java/roomescape/controller/AdminController.java b/src/main/java/roomescape/controller/AdminPageController.java
similarity index 94%
rename from src/main/java/roomescape/controller/AdminController.java
rename to src/main/java/roomescape/controller/AdminPageController.java
index 4e9a8510fa..9ca15af369 100644
--- a/src/main/java/roomescape/controller/AdminController.java
+++ b/src/main/java/roomescape/controller/AdminPageController.java
@@ -6,7 +6,7 @@
 
 @Controller
 @RequestMapping("/admin")
-public class AdminController {
+public class AdminPageController {
     @GetMapping
     public String mainPage() {
         return "admin/index";
diff --git a/src/main/java/roomescape/controller/UserController.java b/src/main/java/roomescape/controller/UserPageController.java
similarity index 91%
rename from src/main/java/roomescape/controller/UserController.java
rename to src/main/java/roomescape/controller/UserPageController.java
index 86ecc0f8ce..ebd3c6519a 100644
--- a/src/main/java/roomescape/controller/UserController.java
+++ b/src/main/java/roomescape/controller/UserPageController.java
@@ -4,7 +4,7 @@
 import org.springframework.web.bind.annotation.GetMapping;
 
 @Controller
-public class UserController {
+public class UserPageController {
     @GetMapping("/reservation")
     public String reservationPage() {
         return "reservation";
diff --git a/src/test/java/roomescape/controller/AdminControllerTest.java b/src/test/java/roomescape/controller/AdminPageControllerTest.java
similarity index 80%
rename from src/test/java/roomescape/controller/AdminControllerTest.java
rename to src/test/java/roomescape/controller/AdminPageControllerTest.java
index adeb4f8b2c..fdfae03c79 100644
--- a/src/test/java/roomescape/controller/AdminControllerTest.java
+++ b/src/test/java/roomescape/controller/AdminPageControllerTest.java
@@ -4,11 +4,11 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-class AdminControllerTest {
+class AdminPageControllerTest {
     @Test
     @DisplayName("관리자 메인 페이지 경로를 정해진 경로로 매핑한다.")
     void mainPage() {
-        AdminController adminController = new AdminController();
+        AdminPageController adminController = new AdminPageController();
         String mainPage = adminController.mainPage();
         Assertions.assertThat(mainPage)
                 .isEqualTo("admin/index");
@@ -17,7 +17,7 @@ void mainPage() {
     @Test
     @DisplayName("관리자 예약 정보 페이지 경로를 정해진 경로로 매핑한다.")
     void reservationPage() {
-        AdminController adminController = new AdminController();
+        AdminPageController adminController = new AdminPageController();
         String reservationPage = adminController.reservationPage();
         Assertions.assertThat(reservationPage)
                 .isEqualTo("admin/reservation-new");
@@ -26,7 +26,7 @@ void reservationPage() {
     @Test
     @DisplayName("시간 관리 페이지 경로를 정해진 경로로 매핑한다.")
     void reservationTimePage() {
-        AdminController adminController = new AdminController();
+        AdminPageController adminController = new AdminPageController();
         String reservationTimePage = adminController.reservationTimePage();
         Assertions.assertThat(reservationTimePage)
                 .isEqualTo("admin/time");

From e12b17e86f564fff407dd8b04ae65b24fc64668d Mon Sep 17 00:00:00 2001
From: robinjoon <robin980108@naver.com>
Date: Tue, 7 May 2024 13:57:50 +0900
Subject: [PATCH 75/75] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20?=
 =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=8B=9C=EA=B0=84=20=EC=A1=B0?=
 =?UTF-8?q?=ED=9A=8C=20API=20=EB=AA=85=EC=84=B8=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                                     |  2 +-
 .../controller/AvailableTimeController.java   | 26 -------------------
 .../controller/ReservationTimeController.java | 14 +++++++++-
 .../resources/static/js/user-reservation.js   |  2 +-
 .../ReservationTimeControllerTest.java        |  7 ++++-
 5 files changed, 21 insertions(+), 30 deletions(-)
 delete mode 100644 src/main/java/roomescape/controller/AvailableTimeController.java

diff --git a/README.md b/README.md
index f3265c68e4..f81fffe0bc 100644
--- a/README.md
+++ b/README.md
@@ -230,7 +230,7 @@
 
 ### Request
 
-> GET /availableTimes?date=${date}&themeId=${themeId}
+> GET /times/book-able?date=${date}&themeId=${themeId}
 
 ### response
 
diff --git a/src/main/java/roomescape/controller/AvailableTimeController.java b/src/main/java/roomescape/controller/AvailableTimeController.java
deleted file mode 100644
index ffc8f65829..0000000000
--- a/src/main/java/roomescape/controller/AvailableTimeController.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package roomescape.controller;
-
-import java.time.LocalDate;
-import java.util.List;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-import roomescape.dto.AvailableTimeResponse;
-import roomescape.service.AvailableTimeService;
-
-@RestController
-@RequestMapping("/availableTimes")
-public class AvailableTimeController {
-
-    private final AvailableTimeService availableTimeService;
-
-    public AvailableTimeController(AvailableTimeService availableTimeService) {
-        this.availableTimeService = availableTimeService;
-    }
-
-    @GetMapping()
-    public List<AvailableTimeResponse> findByThemeAndDate(@RequestParam LocalDate date, @RequestParam long themeId) {
-        return availableTimeService.findByThemeAndDate(date, themeId);
-    }
-}
diff --git a/src/main/java/roomescape/controller/ReservationTimeController.java b/src/main/java/roomescape/controller/ReservationTimeController.java
index 868e43f4b1..bc247105ad 100644
--- a/src/main/java/roomescape/controller/ReservationTimeController.java
+++ b/src/main/java/roomescape/controller/ReservationTimeController.java
@@ -1,6 +1,7 @@
 package roomescape.controller;
 
 import java.net.URI;
+import java.time.LocalDate;
 import java.util.List;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -9,18 +10,24 @@
 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.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import roomescape.dto.AvailableTimeResponse;
 import roomescape.dto.ReservationTimeRequest;
 import roomescape.dto.ReservationTimeResponse;
+import roomescape.service.AvailableTimeService;
 import roomescape.service.ReservationTimeService;
 
 @RestController
 @RequestMapping("/times")
 public class ReservationTimeController {
     private final ReservationTimeService reservationTimeService;
+    private final AvailableTimeService availableTimeService;
 
-    public ReservationTimeController(ReservationTimeService reservationTimeService) {
+    public ReservationTimeController(ReservationTimeService reservationTimeService,
+                                     AvailableTimeService availableTimeService) {
         this.reservationTimeService = reservationTimeService;
+        this.availableTimeService = availableTimeService;
     }
 
     @PostMapping
@@ -35,6 +42,11 @@ public List<ReservationTimeResponse> findAll() {
         return reservationTimeService.findAll();
     }
 
+    @GetMapping("/book-able")
+    public List<AvailableTimeResponse> findByThemeAndDate(@RequestParam LocalDate date, @RequestParam long themeId) {
+        return availableTimeService.findByThemeAndDate(date, themeId);
+    }
+
     @DeleteMapping("/{id}")
     public ResponseEntity<Void> delete(@PathVariable long id) {
         reservationTimeService.delete(id);
diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js
index 12a26c866a..27b4c5ef94 100644
--- a/src/main/resources/static/js/user-reservation.js
+++ b/src/main/resources/static/js/user-reservation.js
@@ -82,7 +82,7 @@ function checkDateAndTheme() {
 }
 
 function fetchAvailableTimes(date, themeId) {
-  fetch(`/availableTimes?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
+  fetch(`/times/book-able?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
     method: 'GET',
     headers: {
       'Content-Type': 'application/json',
diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
index c6082b30a5..9bf390eb6a 100644
--- a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
+++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java
@@ -12,6 +12,8 @@
 import roomescape.dto.ReservationTimeResponse;
 import roomescape.repository.CollectionReservationRepository;
 import roomescape.repository.CollectionReservationTimeRepository;
+import roomescape.repository.CollectionThemeRepository;
+import roomescape.service.AvailableTimeService;
 import roomescape.service.ReservationTimeService;
 
 class ReservationTimeControllerTest {
@@ -25,7 +27,10 @@ void init() {
         CollectionReservationRepository reservationRepository = new CollectionReservationRepository();
         ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository,
                 reservationTimeRepository);
-        reservationTimeController = new ReservationTimeController(reservationTimeService);
+        CollectionThemeRepository themeRepository = new CollectionThemeRepository();
+        AvailableTimeService availableTimeService = new AvailableTimeService(reservationTimeRepository,
+                themeRepository);
+        reservationTimeController = new ReservationTimeController(reservationTimeService, availableTimeService);
     }
 
     @Test