diff --git a/README.md b/README.md new file mode 100644 index 0000000000..f81fffe0bc --- /dev/null +++ b/README.md @@ -0,0 +1,248 @@ +# 요구사항 문서 + +- [x] API 명세를 현재 프론트엔드 코드가 잘 동작할 수 있도록 수정 +- [x] 예약 시간에 대한 제약 조건 추가 + - [x] 중복된 예약 시간 생성 요청 시 에러 + - [x] ISO 8601 표준에 따른 hh:mm 포맷에 해당하지 않는 요청 시 에러 + - [x] 예약이 있는 예약 시간을 삭제 요청 시 에러 +- [x] 예약에 대한 제약 조건 추가 + - [x] 동일한 날짜와 시간, 테마에 예약 생성 요청 시 에러 + - [x] 존재하지 않는 시간에 예약 생성 요청 시 에러 + - [x] ISO 8601 표준에 따른 YYYY-MM-dd 포맷에 해당하지 않는 날짜가 포함된 예약 생성 요청 시 에러 + - [x] 지나간 날짜와 시간의 예약 요청 시 에러 + - [x] 이름이 비어있는 예약 요청 시 에러 + - [x] 존재하지 않는 테마 예약 생성 요청시 에러 + - [x] 테마 값이 비어있는 예약 요청 시 에러 + +- [x] 테마에 대한 제약 조건 추가 + - [x] 테마 이름, 설명, 썸네일 이미자가 비어 있을 경우 에러 + - [x] 중복된 이름의 테마 생성 요청시 에러 + - [x] 예약이 있는 테마를 삭제 요청시 에러 + +- [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" + } + "theme" : { + "id": 1, + "name": "이름", + "description": "설명", + "thumbnail": "썸네일" + } + } +] +``` + +## 예약 추가 API + +### Request + +> POST /reservations HTTP/1.1 +> +> content-type: application/json + +```JSON +{ + "date": "2023-08-05", + "name": "브라운", + "timeId": 1, + "themeId": 1 +} +``` + +### Response + +> HTTP/1.1 201 +> +> Content-Type: application/json +> Location: /reservations/{id} + +```JSON +{ + "id": 1, + "name": "브라운", + "date": "2023-08-05", + "time": { + "id": 1, + "startAt": "10:00" + }, + "theme": { + "id": 1, + "name": "이름", + "description": "설명", + "thumbnail": "썸네일" + } +} +``` + +## 예약 취소 API + +### Request + +> DELETE /reservations/1 HTTP/1.1 + +### Response + +> HTTP/1.1 204 + +## 시간 추가 API + +### request + +> POST /times HTTP/1.1 +> content-type: application/json + +```JSON +{ + "startAt": "10:00" +} +``` + +### response + +> HTTP/1.1 201 +> Content-Type: application/json +> Location: /times/{id} + +```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 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 + +## 예약 가능 시간 조회 API + +### Request + +> GET /times/book-able?date=${date}&themeId=${themeId} + +### response + +> HTTP/1.1 200 +> Content-Type: application/json + +```json +[ + { + "id": 0, + "startAt": "02:53", + "isBooked": false + } +] +``` diff --git a/REFACTORING_LIST.md b/REFACTORING_LIST.md new file mode 100644 index 0000000000..2882536e81 --- /dev/null +++ b/REFACTORING_LIST.md @@ -0,0 +1,7 @@ +## 리팩토링 대상 목록 + +### 우선순위 높음 + +1. Repository.findAll() + Stream 을 사용한 코드 +2. SQL 표준 작성 방식에 따라 대소문자 통일 +3. ~~Repository 의 인메모리 구현체 대신 다른 종류의 테스트 더블 사용하기~~ diff --git a/src/main/java/roomescape/config/TimeFormatterConfig.java b/src/main/java/roomescape/config/TimeFormatterConfig.java new file mode 100644 index 0000000000..7b301a6547 --- /dev/null +++ b/src/main/java/roomescape/config/TimeFormatterConfig.java @@ -0,0 +1,23 @@ +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; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimeFormatterConfig { + 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(TIME_FORMATTER), + new LocalDateSerializer(DATE_FORMATTER)) + .deserializers(new LocalTimeDeserializer(TIME_FORMATTER), new LocalDateDeserializer(DATE_FORMATTER)); + } +} diff --git a/src/main/java/roomescape/controller/AdminPageController.java b/src/main/java/roomescape/controller/AdminPageController.java new file mode 100644 index 0000000000..9ca15af369 --- /dev/null +++ b/src/main/java/roomescape/controller/AdminPageController.java @@ -0,0 +1,29 @@ +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 AdminPageController { + @GetMapping + public String mainPage() { + return "admin/index"; + } + + @GetMapping("/reservation") + public String reservationPage() { + return "admin/reservation-new"; + } + + @GetMapping("/time") + public String reservationTimePage() { + return "admin/time"; + } + + @GetMapping("/theme") + public String themePage() { + return "admin/theme"; + } +} diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java new file mode 100644 index 0000000000..349124b6d0 --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -0,0 +1,43 @@ +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.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 ResponseEntity saveReservation(@RequestBody ReservationRequest reservationRequest) { + ReservationResponse saved = reservationService.save(reservationRequest); + return ResponseEntity.created(URI.create("/reservations/" + saved.id())) + .body(saved); + } + + @GetMapping + public List findAllReservations() { + return reservationService.findAll(); + } + + @DeleteMapping("/{id}") + public ResponseEntity 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 new file mode 100644 index 0000000000..bc247105ad --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationTimeController.java @@ -0,0 +1,55 @@ +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; +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.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, + AvailableTimeService availableTimeService) { + this.reservationTimeService = reservationTimeService; + this.availableTimeService = availableTimeService; + } + + @PostMapping + public ResponseEntity save(@RequestBody ReservationTimeRequest reservationTimeRequest) { + ReservationTimeResponse saved = reservationTimeService.save(reservationTimeRequest); + return ResponseEntity.created(URI.create("/times/" + saved.id())) + .body(saved); + } + + @GetMapping + public List findAll() { + return reservationTimeService.findAll(); + } + + @GetMapping("/book-able") + public List findByThemeAndDate(@RequestParam LocalDate date, @RequestParam long themeId) { + return availableTimeService.findByThemeAndDate(date, themeId); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable long id) { + reservationTimeService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/controller/ThemeController.java b/src/main/java/roomescape/controller/ThemeController.java new file mode 100644 index 0000000000..73c765d680 --- /dev/null +++ b/src/main/java/roomescape/controller/ThemeController.java @@ -0,0 +1,52 @@ +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; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import roomescape.dto.ThemeRequest; +import roomescape.dto.ThemeResponse; +import roomescape.service.ThemeService; + +@RestController +@RequestMapping("/themes") +public class ThemeController { + private final ThemeService themeService; + + public ThemeController(ThemeService themeService) { + this.themeService = themeService; + } + + @GetMapping + public List findAll() { + return themeService.findAll(); + } + + @GetMapping("/ranking") + public List findAndOrderByPopularity(@RequestParam LocalDate start, + @RequestParam LocalDate end, + @RequestParam int count) { + return themeService.findAndOrderByPopularity(start, end, count); + } + + @PostMapping + public ResponseEntity save(@RequestBody ThemeRequest themeRequest) { + ThemeResponse saved = themeService.save(themeRequest); + return ResponseEntity.created(URI.create("/themes/" + saved.id())) + .body(saved); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable long id) { + themeService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/controller/UserPageController.java b/src/main/java/roomescape/controller/UserPageController.java new file mode 100644 index 0000000000..ebd3c6519a --- /dev/null +++ b/src/main/java/roomescape/controller/UserPageController.java @@ -0,0 +1,17 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class UserPageController { + @GetMapping("/reservation") + public String reservationPage() { + return "reservation"; + } + + @GetMapping("/") + public String bestThemePage() { + return "index"; + } +} diff --git a/src/main/java/roomescape/domain/Reservation.java b/src/main/java/roomescape/domain/Reservation.java new file mode 100644 index 0000000000..0c8a38037f --- /dev/null +++ b/src/main/java/roomescape/domain/Reservation.java @@ -0,0 +1,158 @@ +package roomescape.domain; + +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; +import java.time.LocalTime; +import java.util.Objects; +import roomescape.exception.RoomescapeException; + +public class Reservation implements Comparable { + private final Long id; + private final String name; + private final LocalDate date; + private final ReservationTime time; + private final Theme theme; + + 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, Theme theme) { + validateName(name); + validateDate(date); + validateTime(time); + validateTheme(theme); + this.id = id; + this.name = name; + this.date = date; + this.time = time; + this.theme = theme; + } + + private void validateTheme(Theme theme) { + if (theme == null) { + throw new RoomescapeException(EMPTY_THEME); + } + } + + private void validateTime(ReservationTime time) { + if (time == null) { + throw new RoomescapeException(EMPTY_TIME); + } + } + + private void validateDate(LocalDate date) { + if (date == null) { + throw new RoomescapeException(EMPTY_DATE); + } + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new RoomescapeException(EMPTY_NAME); + } + } + + public Reservation(long id, Reservation reservationBeforeSave) { + this(id, reservationBeforeSave.name, reservationBeforeSave.date, reservationBeforeSave.time, + reservationBeforeSave.theme); + } + + @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 isBefore(LocalDateTime base) { + return LocalDateTime.of(date, getTime()).isBefore(base); + } + + 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); + } + + 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; + } + + public String getName() { + return name; + } + + public LocalDate getDate() { + return date; + } + + public ReservationTime getReservationTime() { + return time; + } + + public Theme getTheme() { + return theme; + } + + @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; + + return Objects.equals(id, that.id); + } + + @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..7ca9dd0b5f --- /dev/null +++ b/src/main/java/roomescape/domain/ReservationTime.java @@ -0,0 +1,65 @@ +package roomescape.domain; + +import static roomescape.exception.ExceptionType.EMPTY_TIME; + +import java.time.LocalTime; +import java.util.Objects; +import roomescape.exception.RoomescapeException; + +public class ReservationTime { + private final Long id; + private final LocalTime startAt; + + public ReservationTime(LocalTime startAt) { + this(null, startAt); + } + + public ReservationTime(Long id, LocalTime startAt) { + if (startAt == null) { + throw new RoomescapeException(EMPTY_TIME); + } + this.id = id; + this.startAt = startAt; + } + + public boolean isIdOf(long id) { + return this.id == id; + } + + 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; + + return Objects.equals(id, that.id); + } + + @Override + public String toString() { + return "ReservationTime{" + + "id=" + id + + ", startAt=" + startAt + + '}'; + } +} diff --git a/src/main/java/roomescape/domain/Theme.java b/src/main/java/roomescape/domain/Theme.java new file mode 100644 index 0000000000..ac1d854e09 --- /dev/null +++ b/src/main/java/roomescape/domain/Theme.java @@ -0,0 +1,99 @@ +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 static roomescape.exception.ExceptionType.NOT_URL_BASE_THUMBNAIL; + +import java.util.Objects; +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()) { + throw new RoomescapeException(EMPTY_NAME); + } + } + + private void validateDescription(String description) { + if (description == null || description.isBlank()) { + throw new RoomescapeException(EMPTY_DESCRIPTION); + } + } + + 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) { + this(null, name, description, thumbnail); + } + + public boolean isIdOf(long id) { + return this.id == id; + } + + public boolean isNameOf(String name) { + return this.name.equals(name); + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + 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); + } +} 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/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) { +} diff --git a/src/main/java/roomescape/dto/ReservationRequest.java b/src/main/java/roomescape/dto/ReservationRequest.java new file mode 100644 index 0000000000..fe3e4ec125 --- /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, long themeId) { +} diff --git a/src/main/java/roomescape/dto/ReservationResponse.java b/src/main/java/roomescape/dto/ReservationResponse.java new file mode 100644 index 0000000000..20bd66cb9a --- /dev/null +++ b/src/main/java/roomescape/dto/ReservationResponse.java @@ -0,0 +1,7 @@ +package roomescape.dto; + +import java.time.LocalDate; + +public record ReservationResponse(long id, String name, LocalDate date, ReservationTimeResponse time, + ThemeResponse theme) { +} 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/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 new file mode 100644 index 0000000000..7f71d71bcf --- /dev/null +++ b/src/main/java/roomescape/exception/ExceptionType.java @@ -0,0 +1,41 @@ +package roomescape.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +import org.springframework.http.HttpStatus; + +public enum ExceptionType { + EMPTY_NAME(BAD_REQUEST, "이름은 필수 값입니다."), + EMPTY_TIME(BAD_REQUEST, "시작 시간은 필수 값입니다."), + EMPTY_DATE(BAD_REQUEST, "날짜는 필수값 입니다."), + 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, "이미 예약시간이 존재합니다."), + DUPLICATE_THEME(BAD_REQUEST, "이미 동일한 테마가 존재합니다."), + INVALID_DATE_TIME_FORMAT(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; + 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/exception/RoomescapeExceptionHandler.java b/src/main/java/roomescape/exception/RoomescapeExceptionHandler.java new file mode 100644 index 0000000000..c2a49f1fc9 --- /dev/null +++ b/src/main/java/roomescape/exception/RoomescapeExceptionHandler.java @@ -0,0 +1,28 @@ +package roomescape.exception; + +import static roomescape.exception.ExceptionType.INVALID_DATE_TIME_FORMAT; + +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 handle(HttpMessageNotReadableException e) { + e.printStackTrace(); + return ResponseEntity.status(INVALID_DATE_TIME_FORMAT.getStatus()) + .body(new ErrorResponse(INVALID_DATE_TIME_FORMAT.getMessage())); + } + + @ExceptionHandler(RoomescapeException.class) + public ResponseEntity handle(RoomescapeException e) { + e.printStackTrace(); + return ResponseEntity + .status(e.getHttpStatus()) + .body(new ErrorResponse(e.getMessage())); + } +} diff --git a/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java new file mode 100644 index 0000000000..c462b1f528 --- /dev/null +++ b/src/main/java/roomescape/repository/JdbcTemplateReservationRepository.java @@ -0,0 +1,94 @@ +package roomescape.repository; + +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 +public class JdbcTemplateReservationRepository implements ReservationRepository { + private final JdbcTemplate jdbcTemplate; + private final ReservationRowMapper reservationRowMapper; + + public JdbcTemplateReservationRepository(JdbcTemplate jdbcTemplate, ReservationRowMapper reservationRowMapper) { + this.jdbcTemplate = jdbcTemplate; + this.reservationRowMapper = reservationRowMapper; + } + + @Override + public Reservation save(Reservation reservation) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + save(reservation, keyHolder); + long id = keyHolder.getKey().longValue(); + return new Reservation(id, reservation); + } + + private void save(Reservation reservation, KeyHolder keyHolder) { + jdbcTemplate.update(con -> { + 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); + } + + @Override + public List 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 + """; + 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 new file mode 100644 index 0000000000..3e001eb47d --- /dev/null +++ b/src/main/java/roomescape/repository/JdbcTemplateReservationTimeRepository.java @@ -0,0 +1,84 @@ +package roomescape.repository; + +import java.sql.PreparedStatement; +import java.sql.Time; +import java.time.LocalDate; +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; +import org.springframework.stereotype.Repository; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; +import roomescape.repository.rowmapper.ReservationTimeRowMapper; + +@Repository +public class JdbcTemplateReservationTimeRepository implements ReservationTimeRepository { + private final JdbcTemplate jdbcTemplate; + private final ReservationTimeRowMapper reservationTimeRowMapper; + + 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); + 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) { + 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 findById(long id) { + String sql = "SELECT id, start_at FROM reservation_time WHERE id = ?"; + return jdbcTemplate.query(sql, reservationTimeRowMapper, id) + .stream() + .findAny(); + } + + @Override + public List findAll() { + String sql = "SELECT id, start_at FROM reservation_time"; + return jdbcTemplate.query(sql, reservationTimeRowMapper); + } + + @Override + public List 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 = ?"; + jdbcTemplate.update(sql, id); + } +} diff --git a/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java new file mode 100644 index 0000000000..c9871e1eda --- /dev/null +++ b/src/main/java/roomescape/repository/JdbcTemplateThemeRepository.java @@ -0,0 +1,81 @@ +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.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 ThemeRowMapper themeRowMapper; + + public JdbcTemplateThemeRepository(JdbcTemplate jdbcTemplate, ThemeRowMapper themeRowMapper) { + this.jdbcTemplate = jdbcTemplate; + this.themeRowMapper = themeRowMapper; + } + + @Override + public List findAll() { + String sql = "SELECT id, name, description, thumbnail FROM theme"; + return jdbcTemplate.query(sql, themeRowMapper); + } + + @Override + public List 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 ? + """; + return jdbcTemplate.query(sql, themeRowMapper, start, end, count); + } + + @Override + public Optional findById(long id) { + String sql = "SELECT id, name, description, thumbnail FROM theme WHERE id = ?"; + return jdbcTemplate.query(sql, themeRowMapper, id) + .stream() + .findAny(); + } + + @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 -> { + 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) { + String sql = "DELETE FROM theme WHERE id = ?"; + jdbcTemplate.update(sql, 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..c224f4975a --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -0,0 +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 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 new file mode 100644 index 0000000000..e033a38451 --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -0,0 +1,22 @@ +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); + + boolean existsByStartAt(LocalTime startAt); + + Optional findById(long id); + + List findAll(); + + List findUsedTimeByDateAndTheme(LocalDate date, Theme theme); + + void delete(long 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..f9cdad4015 --- /dev/null +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -0,0 +1,18 @@ +package roomescape.repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import roomescape.domain.Theme; + +public interface ThemeRepository { + List findAll(); + + List findAndOrderByPopularity(LocalDate start, LocalDate end, int count); + + Optional findById(long id); + + Theme save(Theme theme); + + void delete(long 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 { + @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 { + @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 { + @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); + } +} diff --git a/src/main/java/roomescape/service/AvailableTimeService.java b/src/main/java/roomescape/service/AvailableTimeService.java new file mode 100644 index 0000000000..cd7d328b43 --- /dev/null +++ b/src/main/java/roomescape/service/AvailableTimeService.java @@ -0,0 +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 org.springframework.stereotype.Service; +import roomescape.domain.ReservationTime; +import roomescape.domain.Theme; +import roomescape.dto.AvailableTimeResponse; +import roomescape.exception.RoomescapeException; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ThemeRepository; + +@Service +public class AvailableTimeService { + private final ReservationTimeRepository reservationTimeRepository; + private final ThemeRepository themeRepository; + + public AvailableTimeService(ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository) { + this.reservationTimeRepository = reservationTimeRepository; + this.themeRepository = themeRepository; + } + + public List findByThemeAndDate(LocalDate date, long themeId) { + 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 -> toResponse(alreadyUsedTimes, reservationTime)) + .toList(); + } + + private AvailableTimeResponse toResponse(Set 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 new file mode 100644 index 0000000000..a4160cebe8 --- /dev/null +++ b/src/main/java/roomescape/service/ReservationService.java @@ -0,0 +1,92 @@ +package roomescape.service; + +import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION; +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.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; +import roomescape.dto.ReservationRequest; +import roomescape.dto.ReservationResponse; +import roomescape.dto.ReservationTimeResponse; +import roomescape.dto.ThemeResponse; +import roomescape.exception.RoomescapeException; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ThemeRepository; + +@Service +@Transactional +public class ReservationService { + private final ReservationRepository reservationRepository; + private final ReservationTimeRepository reservationTimeRepository; + private final ThemeRepository themeRepository; + + public ReservationService(ReservationRepository reservationRepository, + ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository) { + this.reservationRepository = reservationRepository; + this.reservationTimeRepository = reservationTimeRepository; + this.themeRepository = themeRepository; + } + + public ReservationResponse save(ReservationRequest reservationRequest) { + ReservationTime requestedTime = reservationTimeRepository.findById(reservationRequest.timeId()) + .orElseThrow(() -> new RoomescapeException(NOT_FOUND_RESERVATION_TIME)); + Theme requestedTheme = themeRepository.findById(reservationRequest.themeId()) + .orElseThrow(() -> new RoomescapeException(NOT_FOUND_THEME)); + + Reservation beforeSave = new Reservation( + reservationRequest.name(), + reservationRequest.date(), + requestedTime, + requestedTheme + ); + + 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); + } + } + + 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, themeResponse); + } + + public List 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..b4e6b98289 --- /dev/null +++ b/src/main/java/roomescape/service/ReservationTimeService.java @@ -0,0 +1,62 @@ +package roomescape.service; + +import static roomescape.exception.ExceptionType.DELETE_USED_TIME; +import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME; + +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.domain.ReservationTime; +import roomescape.dto.ReservationTimeRequest; +import roomescape.dto.ReservationTimeResponse; +import roomescape.exception.RoomescapeException; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; + +@Service +@Transactional +public class ReservationTimeService { + private final ReservationRepository reservationRepository; + private final ReservationTimeRepository reservationTimeRepository; + + public ReservationTimeService(ReservationRepository reservationRepository, + ReservationTimeRepository reservationTimeRepository) { + this.reservationRepository = reservationRepository; + this.reservationTimeRepository = reservationTimeRepository; + } + + public ReservationTimeResponse save(ReservationTimeRequest reservationTimeRequest) { + if (reservationTimeRepository.existsByStartAt(reservationTimeRequest.startAt())) { + throw new RoomescapeException(DUPLICATE_RESERVATION_TIME); + } + ReservationTime reservationTime = new ReservationTime(reservationTimeRequest.startAt()); + ReservationTime saved = reservationTimeRepository.save(reservationTime); + return toResponse(saved); + } + + private ReservationTimeResponse toResponse(ReservationTime saved) { + return new ReservationTimeResponse(saved.getId(), saved.getStartAt()); + } + + public List findAll() { + return reservationTimeRepository.findAll().stream() + .map(this::toResponse) + .toList(); + } + + public void delete(long id) { + validateUsedTime(id); + reservationTimeRepository.delete(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 new file mode 100644 index 0000000000..7556b42577 --- /dev/null +++ b/src/main/java/roomescape/service/ThemeService.java @@ -0,0 +1,71 @@ +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 org.springframework.transaction.annotation.Transactional; +import roomescape.domain.Theme; +import roomescape.dto.ThemeRequest; +import roomescape.dto.ThemeResponse; +import roomescape.exception.RoomescapeException; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ThemeRepository; + +@Service +@Transactional +public class ThemeService { + + private final ThemeRepository themeRepository; + private final ReservationRepository reservationRepository; + + public ThemeService(ThemeRepository themeRepository, ReservationRepository reservationRepository) { + this.themeRepository = themeRepository; + this.reservationRepository = reservationRepository; + } + + public ThemeResponse save(ThemeRequest themeRequest) { + boolean hasDuplicateTheme = themeRepository.findAll().stream() + .anyMatch(theme -> theme.isNameOf(themeRequest.name())); + if (hasDuplicateTheme) { + throw new RoomescapeException(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 findAll() { + return themeRepository.findAll().stream() + .map(this::toResponse) + .toList(); + } + + public List findAndOrderByPopularity(LocalDate start, LocalDate end, int count) { + return themeRepository.findAndOrderByPopularity(start, end, count).stream() + .map(this::toResponse) + .toList(); + } + + public void delete(long id) { + validateUsedTheme(id); + themeRepository.delete(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/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 diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000000..991d50c4a7 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,27 @@ +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 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, + theme_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (time_id) REFERENCES reservation_time (id), + FOREIGN KEY (theme_id) REFERENCES theme (id) +); diff --git a/src/main/resources/static/js/ranking.js b/src/main/resources/static/js/ranking.js index dee05edf0b..ba5dda7a9a 100644 --- a/src/main/resources/static/js/ranking.js +++ b/src/main/resources/static/js/ranking.js @@ -1,25 +1,34 @@ document.addEventListener('DOMContentLoaded', () => { - /* - TODO: [3단계] 인기 테마 - 인기 테마 목록 조회 API 호출 - */ - requestRead('/') // 인기 테마 목록 조회 API endpoint - .then(render) - .catch(error => console.error('Error fetching times:', error)); + 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'); + + data.forEach(theme => { + const name = theme.name; + const thumbnail = theme.thumbnail; + const description = theme.description; + + const htmlContent = ` ${name}
${name}
@@ -27,18 +36,18 @@ function render(data) {
`; - 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/main/resources/static/js/reservation-new.js b/src/main/resources/static/js/reservation-new.js index 098b8b70d8..fd87452bf6 100644 --- a/src/main/resources/static/js/reservation-new.js +++ b/src/main/resources/static/js/reservation-new.js @@ -23,10 +23,7 @@ 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; // 예약자명 row.insertCell(2).textContent = item.theme.name; // 테마명 diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js index 89ff141af8..27b4c5ef94 100644 --- a/src/main/resources/static/js/user-reservation.js +++ b/src/main/resources/static/js/user-reservation.js @@ -36,13 +36,8 @@ function renderTheme(themes) { const themeSlots = document.getElementById('theme-slots'); themeSlots.innerHTML = ''; themes.forEach(theme => { - const name = ''; - const themeId = ''; - /* - TODO: [3단계] 사용자 예약 - 테마 목록 조회 API 호출 후 렌더링 - response 명세에 맞춰 createSlot 함수 호출 시 값 설정 - createSlot('theme', theme name, theme id) 형태로 호출 - */ + const name = theme.name; + const themeId = theme.id; themeSlots.appendChild(createSlot('theme', name, themeId)); }); } @@ -87,11 +82,7 @@ function checkDateAndTheme() { } function fetchAvailableTimes(date, themeId) { - /* - TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 - 요청 포맷에 맞게 설정 - */ - fetch('/', { // 예약 가능 시간 조회 API endpoint + fetch(`/times/book-able?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint method: 'GET', headers: { 'Content-Type': 'application/json', @@ -100,7 +91,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) { @@ -116,13 +107,9 @@ function renderAvailableTimes(times) { return; } times.forEach(time => { - /* - 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); @@ -158,8 +145,7 @@ function onReservationButtonClick() { if (selectedDate && selectedThemeId && selectedTimeId) { /* - TODO: [3단계] 사용자 예약 - 예약 요청 API 호출 - [5단계] 예약 생성 기능 변경 - 사용자 + TODO: [5단계] 예약 생성 기능 변경 - 사용자 request 명세에 맞게 설정 */ const reservationData = { 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/AdminPageControllerTest.java b/src/test/java/roomescape/controller/AdminPageControllerTest.java new file mode 100644 index 0000000000..fdfae03c79 --- /dev/null +++ b/src/test/java/roomescape/controller/AdminPageControllerTest.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 AdminPageControllerTest { + @Test + @DisplayName("관리자 메인 페이지 경로를 정해진 경로로 매핑한다.") + void mainPage() { + AdminPageController adminController = new AdminPageController(); + String mainPage = adminController.mainPage(); + Assertions.assertThat(mainPage) + .isEqualTo("admin/index"); + } + + @Test + @DisplayName("관리자 예약 정보 페이지 경로를 정해진 경로로 매핑한다.") + void reservationPage() { + AdminPageController adminController = new AdminPageController(); + String reservationPage = adminController.reservationPage(); + Assertions.assertThat(reservationPage) + .isEqualTo("admin/reservation-new"); + } + + @Test + @DisplayName("시간 관리 페이지 경로를 정해진 경로로 매핑한다.") + void reservationTimePage() { + AdminPageController adminController = new AdminPageController(); + 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..60a44b2504 --- /dev/null +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -0,0 +1,108 @@ +package roomescape.controller; + +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.time.LocalTime; +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; +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; + +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", "http://thumbnail"); + + private CollectionReservationRepository collectionReservationRepository; + private ReservationController reservationController; + + @BeforeEach + void initController() { + CollectionReservationTimeRepository timeRepository = new CollectionReservationTimeRepository(); + CollectionThemeRepository themeRepository = new CollectionThemeRepository(); + collectionReservationRepository = new CollectionReservationRepository(); + ReservationService reservationService = new ReservationService(collectionReservationRepository, timeRepository, + themeRepository); + reservationController = new ReservationController(reservationService); + + defaultTime = timeRepository.save(defaultTime); + defualtTheme = themeRepository.save(defualtTheme); + } + + @Test + @DisplayName("예약 정보를 잘 저장하는지 확인한다.") + void saveReservation() { + //given + LocalDate date = LocalDate.now().plusDays(1); + + //when + ReservationResponse saveResponse = reservationController.saveReservation( + new ReservationRequest(date, "폴라", TIME_ID, defualtTheme.getId())) + .getBody(); + + long id = Objects.requireNonNull(saveResponse).id(); + + //then + ReservationResponse expected = new ReservationResponse(id, "폴라", date, + new ReservationTimeResponse(TIME_ID, TIME), + new ThemeResponse(defualtTheme.getId(), defualtTheme.getName(), defualtTheme.getDescription(), + defualtTheme.getThumbnail())); + + Assertions.assertThat(saveResponse).isEqualTo(expected); + } + + @Test + @DisplayName("예약 정보를 잘 불러오는지 확인한다.") + void findAllReservations() { + //when + List allReservations = reservationController.findAllReservations(); + + //then + Assertions.assertThat(allReservations).isEmpty(); + } + + @Test + @DisplayName("예약 정보를 잘 지우는지 확인한다.") + void delete() { + //given + Reservation saved = collectionReservationRepository.save( + new Reservation("폴라", LocalDate.now(), defaultTime, defualtTheme)); + + //when + reservationController.delete(saved.getId()); + + //then + List reservationResponses = reservationController.findAllReservations(); + Assertions.assertThat(reservationResponses) + .isEmpty(); + } + + @Test + @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.") + void checkRepositoryDependency() { + 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..9bf390eb6a --- /dev/null +++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java @@ -0,0 +1,85 @@ +package roomescape.controller; + +import java.lang.reflect.Field; +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 roomescape.domain.ReservationTime; +import roomescape.dto.ReservationTimeRequest; +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 { + + private CollectionReservationTimeRepository reservationTimeRepository; + private ReservationTimeController reservationTimeController; + + @BeforeEach + void init() { + reservationTimeRepository = new CollectionReservationTimeRepository(); + CollectionReservationRepository reservationRepository = new CollectionReservationRepository(); + ReservationTimeService reservationTimeService = new ReservationTimeService(reservationRepository, + reservationTimeRepository); + CollectionThemeRepository themeRepository = new CollectionThemeRepository(); + AvailableTimeService availableTimeService = new AvailableTimeService(reservationTimeRepository, + themeRepository); + reservationTimeController = new ReservationTimeController(reservationTimeService, availableTimeService); + } + + @Test + @DisplayName("시간을 잘 저장하는지 확인한다.") + void save() { + LocalTime time = LocalTime.now(); + ReservationTimeResponse save = reservationTimeController.save(new ReservationTimeRequest(time)).getBody(); + + ReservationTimeResponse expected = new ReservationTimeResponse(save.id(), time); + Assertions.assertThat(save) + .isEqualTo(expected); + } + + @Test + @DisplayName("시간을 잘 불러오는지 확인한다.") + void findAll() { + List reservationTimeResponses = reservationTimeController.findAll(); + + Assertions.assertThat(reservationTimeResponses) + .isEmpty(); + } + + @Test + @DisplayName("시간을 잘 지우는지 확인한다.") + void delete() { + //given + reservationTimeRepository.save(new ReservationTime(1L, LocalTime.now())); + + //when + reservationTimeController.delete(1); + + //then + List reservationTimeResponses = reservationTimeController.findAll(); + Assertions.assertThat(reservationTimeResponses) + .isEmpty(); + } + + @Test + @DisplayName("내부에 Repository를 의존하고 있지 않은지 확인한다.") + void checkRepositoryDependency() { + 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..1abdae0c0b --- /dev/null +++ b/src/test/java/roomescape/domain/ReservationTest.java @@ -0,0 +1,57 @@ +package roomescape.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +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; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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, "이름", "설명", "http://썸네일"); + + @DisplayName("생성 테스트") + @Test + void constructTest() { + assertAll( + () -> assertThatThrownBy(() -> new Reservation(null, DEFAULT_DATE, DEFAULT_TIME, DEFAULT_THEME)) + .isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_NAME.getMessage()), + + () -> assertThatThrownBy(() -> new Reservation("name", null, DEFAULT_TIME, DEFAULT_THEME)) + .isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_DATE.getMessage()), + + () -> assertThatThrownBy(() -> new Reservation("name", DEFAULT_DATE, null, DEFAULT_THEME)) + .isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_TIME.getMessage()), + + () -> assertThatThrownBy(() -> new Reservation("name", DEFAULT_DATE, DEFAULT_TIME, null)) + .isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_THEME.getMessage()) + ); + + } + + @Test + @DisplayName("날짜를 기준으로 비교를 잘 하는지 확인.") + void compareTo() { + Reservation first = new Reservation(1L, "폴라", LocalDate.of(1999, 12, 1), new ReservationTime( + LocalTime.of(16, 30)), DEFAULT_THEME); + Reservation second = new Reservation(2L, "로빈", LocalDate.of(1998, 1, 8), new ReservationTime( + LocalTime.of(16, 30)), DEFAULT_THEME); + int compareTo = first.compareTo(second); + Assertions.assertThat(compareTo) + .isGreaterThan(0); + } +} diff --git a/src/test/java/roomescape/domain/ReservationTimeTest.java b/src/test/java/roomescape/domain/ReservationTimeTest.java new file mode 100644 index 0000000000..afc563831c --- /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 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.RoomescapeException; + +class ReservationTimeTest { + + @DisplayName("시간이 null 인 ReservationTime 을 생성할 수 없다.") + @Test + void startAtMustBeNotNull() { + assertThatThrownBy(() -> new ReservationTime(null)) + .isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_TIME.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..c2b359ae87 --- /dev/null +++ b/src/test/java/roomescape/domain/ThemeTest.java @@ -0,0 +1,62 @@ +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.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; +import roomescape.exception.RoomescapeException; + +class ThemeTest { + + @DisplayName("객체 생성 테스트") + @Test + void constructTest() { + assertAll( + () -> assertThatThrownBy(() -> new Theme(null, "description", "http://thumbnail")) + .isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_NAME.getMessage()), + + () -> 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", "http://thumbnail")) + .doesNotThrowAnyException(), + + () -> assertThatCode(() -> new Theme(null, "name", "description", "http://thumbnail")) + .doesNotThrowAnyException(), + + () -> assertThatCode(() -> new Theme(1L, "name", "description", "http://thumbnail")) + .doesNotThrowAnyException() + ); + } + + @DisplayName("동등성 테스트") + @Test + void equalsTest() { + assertAll( + () -> assertThat(new Theme(1L, "name", "description", "http://thumbnail")) + .isEqualTo(new Theme(1L, "otherName", "otherDescription", "http://otherThumbnail")), + + () -> assertThat(new Theme(1L, "sameName", "sameDescription", "http://sameThumbnail")) + .isNotEqualTo(new Theme(2L, "sameName", "sameDescription", "http://sameThumbnail")), + + () -> assertThat(new Theme(1L, "sameName", "sameDescription", "http://sameThumbnail")) + .isNotEqualTo(new Theme(null, "sameName", "sameDescription", "http://sameThumbnail")) + ); + } +} 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..73d9bffb46 --- /dev/null +++ b/src/test/java/roomescape/integration/AdminIntegrationTest.java @@ -0,0 +1,156 @@ +package roomescape.integration; + +import static org.hamcrest.Matchers.is; + +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; +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 { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + @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')"); + 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; + } + + @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 params = new HashMap<>(); + 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) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .body("id", is(1)); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + + RestAssured.given().log().all() + .when().delete("/reservations/1") + .then().log().all() + .statusCode(204); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + + @Test + @DisplayName("관리자 예약 페이지가 DB와 함께 잘 동작한다.") + void adminReservationPageWorkWithDB() { + Map params = new HashMap<>(); + 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) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201); + + 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(204); + + Integer countAfterDelete = jdbcTemplate.queryForObject("SELECT COUNT(1) FROM reservation", Integer.class); + Assertions.assertThat(countAfterDelete).isEqualTo(0); + } + + @Test + @DisplayName("시간 관리 페이지가 잘 동작한다.") + void reservationTimePageWork() { + Map params = new HashMap<>(); + params.put("startAt", "10:00"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/times") + .then().log().all() + .statusCode(201); + + 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(204); + } +} diff --git a/src/test/java/roomescape/repository/CollectionReservationRepository.java b/src/test/java/roomescape/repository/CollectionReservationRepository.java new file mode 100644 index 0000000000..391f8b157a --- /dev/null +++ b/src/test/java/roomescape/repository/CollectionReservationRepository.java @@ -0,0 +1,62 @@ +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 reservations; + private final AtomicLong atomicLong; + + + public CollectionReservationRepository() { + this.reservations = new ArrayList<>(); + this.atomicLong = new AtomicLong(0); + } + + @Override + public Reservation save(Reservation reservation) { + Reservation saved = new Reservation(atomicLong.incrementAndGet(), reservation); + reservations.add(saved); + return saved; + } + + @Override + public List findAll() { + return reservations.stream() + .sorted() + .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() + .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..1008c221f7 --- /dev/null +++ b/src/test/java/roomescape/repository/CollectionReservationTimeRepository.java @@ -0,0 +1,86 @@ +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 reservationTimes; + private final AtomicLong atomicLong; + private final ReservationRepository reservationRepository; + + + public CollectionReservationTimeRepository() { + this(new ArrayList<>()); + } + + public CollectionReservationTimeRepository(List reservationTimes) { + this(reservationTimes, new AtomicLong(0)); + } + + public CollectionReservationTimeRepository(List reservationTimes, AtomicLong atomicLong) { + this(reservationTimes, atomicLong, null); + } + + public CollectionReservationTimeRepository(List 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 + public ReservationTime save(ReservationTime reservationTime) { + ReservationTime saved = new ReservationTime(atomicLong.incrementAndGet(), reservationTime.getStartAt()); + reservationTimes.add(saved); + return saved; + } + + @Override + public boolean existsByStartAt(LocalTime startAt) { + return reservationTimes.stream() + .anyMatch(reservationTime -> startAt.equals(reservationTime.getStartAt())); + } + + @Override + public Optional findById(long id) { + return reservationTimes.stream() + .filter(reservationTime -> reservationTime.isIdOf(id)) + .findFirst(); + } + + @Override + public List findAll() { + return List.copyOf(reservationTimes); + } + + @Override + public List 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() + .filter(reservationTime -> reservationTime.isIdOf(id)) + .findAny() + .ifPresent(reservationTimes::remove); + } +} diff --git a/src/test/java/roomescape/repository/CollectionThemeRepository.java b/src/test/java/roomescape/repository/CollectionThemeRepository.java new file mode 100644 index 0000000000..98ba2856f4 --- /dev/null +++ b/src/test/java/roomescape/repository/CollectionThemeRepository.java @@ -0,0 +1,85 @@ +package roomescape.repository; + + +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 themes; + private final AtomicLong index; + private final CollectionReservationRepository reservationRepository; + + public CollectionThemeRepository() { + this(new ArrayList<>(), new AtomicLong(0), null); + } + + private CollectionThemeRepository(List 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 + public List findAll() { + return new ArrayList<>(themes); + } + + @Override + public List findAndOrderByPopularity(LocalDate start, LocalDate end, int count) { + if (reservationRepository == null) { + throw new UnsupportedOperationException("ReservationRepository 를 사용해 생성하지 않아 메서드를 사용할 수 없습니다."); + } + Map 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 + public Optional findById(long id) { + return themes.stream() + .filter(theme -> theme.isIdOf(id)) + .findFirst(); + } + + @Override + public Theme save(Theme theme) { + 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/repository/JdbcTemplateReservationRepositoryTest.java b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java new file mode 100644 index 0000000000..f51d126f1b --- /dev/null +++ b/src/test/java/roomescape/repository/JdbcTemplateReservationRepositoryTest.java @@ -0,0 +1,129 @@ +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; +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 JdbcTemplateReservationRepositoryTest { + private static final ReservationTime DEFAULT_TIME = new ReservationTime(1L, LocalTime.of(11, 56)); + private static final Theme DEFAULT_THEME = new Theme(1L, "이름", "설명", "http://썸네일"); + + @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')"); + + 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', 'http://thumbnail')"); + + } + + @Test + @DisplayName("Reservation 을 잘 저장하는지 확인한다.") + void save() { + var beforeSave = reservationRepository.findAll(); + Reservation saved = reservationRepository.save( + new Reservation("test", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME)); + var afterSave = reservationRepository.findAll(); + + Assertions.assertThat(afterSave) + .containsAll(beforeSave) + .contains(saved); + } + + @Test + @DisplayName("Reservation 을 잘 조회하는지 확인한다.") + void findAll() { + List beforeSave = reservationRepository.findAll(); + reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME)); + reservationRepository.save(new Reservation("test2", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME)); + + List afterSave = reservationRepository.findAll(); + Assertions.assertThat(afterSave.size()) + .isEqualTo(beforeSave.size() + 2); + } + + @Test + @DisplayName("Reservation 을 잘 지우는지 확인한다.") + void delete() { + List beforeSaveAndDelete = reservationRepository.findAll(); + reservationRepository.save(new Reservation("test", LocalDate.now(), DEFAULT_TIME, DEFAULT_THEME)); + + reservationRepository.delete(1L); + + List afterSaveAndDelete = reservationRepository.findAll(); + + 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 new file mode 100644 index 0000000000..8c29259eb2 --- /dev/null +++ b/src/test/java/roomescape/repository/JdbcTemplateReservationTimeRepositoryTest.java @@ -0,0 +1,106 @@ +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; +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 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"); + } + + @Test + @DisplayName("ReservationTime 을 잘 저장하는지 확인한다.") + void save() { + var beforeSave = reservationTimeRepository.findAll().stream().map(ReservationTime::getId).toList(); + ReservationTime saved = reservationTimeRepository.save(new ReservationTime(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 beforeSave = reservationTimeRepository.findAll(); + reservationTimeRepository.save(new ReservationTime(LocalTime.now())); + reservationTimeRepository.save(new ReservationTime(LocalTime.now())); + + List afterSave = reservationTimeRepository.findAll(); + + Assertions.assertThat(afterSave.size()) + .isEqualTo(beforeSave.size() + 2); + } + + @Test + @DisplayName("ReservationTime 을 잘 지우하는지 확인한다.") + void delete() { + List beforeSaveAndDelete = reservationTimeRepository.findAll(); + reservationTimeRepository.save(new ReservationTime(LocalTime.now())); + + reservationTimeRepository.delete(1L); + + List afterSaveAndDelete = reservationTimeRepository.findAll(); + + 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 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..60ea789407 --- /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 allTheme = themeRepository.findAll(); + + Assertions.assertThat(allTheme) + .containsExactly(theme); + } + + @Test + void findAndOrderByPopularity() { + 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))); + + 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 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", "http://thumbnail")); + + themeRepository.delete(theme.getId()); + + Assertions.assertThat(themeRepository.findAll()) + .isEmpty(); + } +} diff --git a/src/test/java/roomescape/service/AvailableTimeServiceTest.java b/src/test/java/roomescape/service/AvailableTimeServiceTest.java new file mode 100644 index 0000000000..cc953821c4 --- /dev/null +++ b/src/test/java/roomescape/service/AvailableTimeServiceTest.java @@ -0,0 +1,64 @@ +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.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(reservationRepository); + themeRepository = new CollectionThemeRepository(); + availableTimeService = new AvailableTimeService(reservationTimeRepository, themeRepository); + } + + @DisplayName("날짜와 테마, 시간에 대한 예약 내역을 확인할 수 있다.") + @Test + void findAvailableTimeTest() { + //given + 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))); + 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 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) + ); + } +} diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java new file mode 100644 index 0000000000..7780d186e3 --- /dev/null +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -0,0 +1,178 @@ +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.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; +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", "http://thumbnail"); + + @BeforeEach + void initService() { + CollectionReservationTimeRepository reservationTimeRepository = new CollectionReservationTimeRepository(); + ThemeRepository themeRepository = new CollectionThemeRepository(); + reservationRepository = new CollectionReservationRepository(); + 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_RESERVATION.getMessage()); + } + + @DisplayName("존재하지 않는 시간에 대해 예약을 생성하면 예외가 발생한다.") + @Test + void createReservationWithTimeNotExistsTest() { + assertThatThrownBy(() -> reservationService.save(new ReservationRequest( + LocalDate.now().minusDays(1), + "name", + 2L, + defaultTheme.getId() + ))) + .isInstanceOf(RoomescapeException.class) + .hasMessage(NOT_FOUND_RESERVATION_TIME.getMessage()); + } + + @DisplayName("존재하지 않는 테마에 대해 예약을 생성하면 예외가 발생한다.") + @Test + void createReservationWithThemeNotExistsTest() { + assertThatThrownBy(() -> reservationService.save(new ReservationRequest( + LocalDate.now().minusDays(1), + "name", + defaultTime.getId(), + 2L + ))) + .isInstanceOf(RoomescapeException.class) + .hasMessage(NOT_FOUND_THEME.getMessage()); + } + + @DisplayName("필수값이 입력되지 않은 예약 생성 요청에 대해 예외가 발생한다.") + @Test + void emptyRequiredValueTest() { + assertAll( + () -> assertThatThrownBy(() -> reservationService.save(new ReservationRequest( + LocalDate.now().minusDays(1), + null, + defaultTime.getId(), + defaultTheme.getId() + ))).isInstanceOf(RoomescapeException.class) + .hasMessage(EMPTY_NAME.getMessage()) + ); + } + + @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 reservationResponses = reservationService.findAll(); + + //then + assertThat(reservationResponses).hasSize(4); + } + + @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(); + } + } +} diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java new file mode 100644 index 0000000000..38c6c7f1bf --- /dev/null +++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java @@ -0,0 +1,110 @@ +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.DELETE_USED_TIME; +import static roomescape.exception.ExceptionType.DUPLICATE_RESERVATION_TIME; + +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.ReservationTimeRequest; +import roomescape.dto.ReservationTimeResponse; +import roomescape.exception.RoomescapeException; +import roomescape.repository.CollectionReservationRepository; +import roomescape.repository.CollectionReservationTimeRepository; + +class ReservationTimeServiceTest { + + private CollectionReservationRepository reservationRepository; + private CollectionReservationTimeRepository reservationTimeRepository; + private ReservationTimeService reservationTimeService; + + @BeforeEach + void initService() { + reservationRepository = new CollectionReservationRepository(); + reservationTimeRepository = new CollectionReservationTimeRepository(); + 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 reservationTimeResponses = reservationTimeService.findAll(); + + //then + assertThat(reservationTimeResponses) + .hasSize(4); + } + + @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", "http://thumbnail") + )); + + //when & then + assertThatCode(() -> reservationTimeService.delete(1L)) + .isInstanceOf(RoomescapeException.class) + .hasMessage(DELETE_USED_TIME.getMessage()); + } + } +} diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java new file mode 100644 index 0000000000..5ea3366dbf --- /dev/null +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -0,0 +1,157 @@ +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.DELETE_USED_THEME; +import static roomescape.exception.ExceptionType.DUPLICATE_THEME; + +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.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.ThemeRepository; + +class ThemeServiceTest { + + private ThemeRepository themeRepository; + private CollectionReservationTimeRepository reservationTimeRepository; + private CollectionReservationRepository reservationRepository; + private ThemeService themeService; + + @BeforeEach + void initService() { + reservationTimeRepository = new CollectionReservationTimeRepository(); + reservationRepository = new CollectionReservationRepository(); + themeRepository = new CollectionThemeRepository(); + themeService = new ThemeService(themeRepository, reservationRepository); + } + + @DisplayName("테마가 여러개 있으면 테마를 모두 조회할 수 있다.") + @Test + void findAllTest() { + //given + 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 themeResponses = themeService.findAll(); + + //then + 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 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", "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))); + + 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 { + private ReservationTime defaultTime = new ReservationTime(LocalTime.now().plusMinutes(5)); + private Theme defaultTheme = new Theme("name", "description", "http://thumbnail"); + + @BeforeEach + void addDefaultData() { + defaultTime = reservationTimeRepository.save(defaultTime); + defaultTheme = themeRepository.save(defaultTheme); + } + + @DisplayName("동일한 이름의 테마를 예약할 수 없다.") + @Test + void duplicatedThemeSaveFailTest() { + assertThatThrownBy(() -> themeService.save(new ThemeRequest( + defaultTheme.getName(), "description", "http://thumbnail" + ))).isInstanceOf(RoomescapeException.class) + .hasMessage(DUPLICATE_THEME.getMessage()); + } + + @DisplayName("다른 이름의 테마를 예약할 수 있다.") + @Test + void notDuplicatedThemeNameSaveTest() { + themeService.save(new ThemeRequest("otherName", "description", "http://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(DELETE_USED_THEME.getMessage()); + } + + @DisplayName("존재하지 않는 테마 id로 삭제하더라도 오류로 간주하지 않는다.") + @Test + void notExistThemeDeleteTest() { + assertThatCode(() -> themeService.delete(2L)) + .doesNotThrowAnyException(); + } + } +} 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..991d50c4a7 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,27 @@ +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 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, + theme_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (time_id) REFERENCES reservation_time (id), + FOREIGN KEY (theme_id) REFERENCES theme (id) +);