From fae11c42121eb735c7852d5bcc5a769caaeab8a1 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 09:16:58 +0900 Subject: [PATCH 01/17] =?UTF-8?q?refactor:=20theme=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/reservation/domain/Reservation.java | 2 +- .../dto/response/ReservationResponse.java | 2 +- .../reservation/service/ReservationService.java | 4 ++-- .../theme/{service => business}/ThemeService.java | 13 ++++++------- .../persistence}/Theme.java | 2 +- .../persistence}/ThemeRepository.java | 4 +--- .../theme/{controller => web}/ThemeController.java | 7 ++----- .../roomescape/theme/{dto => web}/ThemeRequest.java | 2 +- .../theme/{dto => web}/ThemeResponse.java | 4 ++-- .../theme/{dto => web}/ThemesResponse.java | 2 +- .../payment/business/PaymentServiceTest.java | 4 ++-- .../controller/ReservationControllerTest.java | 6 ++---- .../controller/ReservationTimeControllerTest.java | 4 ++-- .../reservation/domain/ReservationTest.java | 2 +- .../ReservationSearchSpecificationTest.java | 4 ++-- .../reservation/service/ReservationServiceTest.java | 6 +++--- .../service/ReservationTimeServiceTest.java | 4 ++-- .../service/ReservationWithPaymentServiceTest.java | 4 ++-- .../{service => business}/ThemeServiceTest.java | 12 ++++++------ .../{controller => web}/ThemeControllerTest.java | 2 +- src/test/java/roomescape/util/Fixtures.kt | 2 +- 21 files changed, 42 insertions(+), 50 deletions(-) rename src/main/java/roomescape/theme/{service => business}/ThemeService.java (86%) rename src/main/java/roomescape/theme/{domain => infrastructure/persistence}/Theme.java (95%) rename src/main/java/roomescape/theme/{domain/repository => infrastructure/persistence}/ThemeRepository.java (90%) rename src/main/java/roomescape/theme/{controller => web}/ThemeController.java (95%) rename src/main/java/roomescape/theme/{dto => web}/ThemeRequest.java (97%) rename src/main/java/roomescape/theme/{dto => web}/ThemeResponse.java (88%) rename src/main/java/roomescape/theme/{dto => web}/ThemesResponse.java (91%) rename src/test/java/roomescape/theme/{service => business}/ThemeServiceTest.java (94%) rename src/test/java/roomescape/theme/{controller => web}/ThemeControllerTest.java (99%) diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java index 228f5b04..0cb0b972 100644 --- a/src/main/java/roomescape/reservation/domain/Reservation.java +++ b/src/main/java/roomescape/reservation/domain/Reservation.java @@ -18,7 +18,7 @@ import jakarta.persistence.ManyToOne; import roomescape.common.exception.ErrorType; import roomescape.common.exception.RoomescapeException; import roomescape.member.infrastructure.persistence.Member; -import roomescape.theme.domain.Theme; +import roomescape.theme.infrastructure.persistence.Theme; @Entity public class Reservation { diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java index ba4ecb2b..f1fa1704 100644 --- a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import roomescape.member.web.MemberResponse; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; -import roomescape.theme.dto.ThemeResponse; +import roomescape.theme.web.ThemeResponse; @Schema(name = "예약 정보", description = "예약 저장 및 조회 응답에 사용됩니다.") public record ReservationResponse( diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java index 5b8c4efd..17e228a0 100644 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -24,8 +24,8 @@ import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.MyReservationsResponse; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationsResponse; -import roomescape.theme.domain.Theme; -import roomescape.theme.service.ThemeService; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.business.ThemeService; @Service @Transactional diff --git a/src/main/java/roomescape/theme/service/ThemeService.java b/src/main/java/roomescape/theme/business/ThemeService.java similarity index 86% rename from src/main/java/roomescape/theme/service/ThemeService.java rename to src/main/java/roomescape/theme/business/ThemeService.java index aabc2790..0b46b3dc 100644 --- a/src/main/java/roomescape/theme/service/ThemeService.java +++ b/src/main/java/roomescape/theme/business/ThemeService.java @@ -1,4 +1,4 @@ -package roomescape.theme.service; +package roomescape.theme.business; import java.time.LocalDate; import java.util.List; @@ -9,12 +9,11 @@ import org.springframework.transaction.annotation.Transactional; import roomescape.common.exception.ErrorType; import roomescape.common.exception.RoomescapeException; -import roomescape.reservation.domain.repository.ReservationRepository; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; -import roomescape.theme.dto.ThemeRequest; -import roomescape.theme.dto.ThemeResponse; -import roomescape.theme.dto.ThemesResponse; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; +import roomescape.theme.web.ThemeRequest; +import roomescape.theme.web.ThemeResponse; +import roomescape.theme.web.ThemesResponse; @Service @Transactional diff --git a/src/main/java/roomescape/theme/domain/Theme.java b/src/main/java/roomescape/theme/infrastructure/persistence/Theme.java similarity index 95% rename from src/main/java/roomescape/theme/domain/Theme.java rename to src/main/java/roomescape/theme/infrastructure/persistence/Theme.java index 19513cd9..9f5f64c8 100644 --- a/src/main/java/roomescape/theme/domain/Theme.java +++ b/src/main/java/roomescape/theme/infrastructure/persistence/Theme.java @@ -1,4 +1,4 @@ -package roomescape.theme.domain; +package roomescape.theme.infrastructure.persistence; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/roomescape/theme/domain/repository/ThemeRepository.java b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java similarity index 90% rename from src/main/java/roomescape/theme/domain/repository/ThemeRepository.java rename to src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java index 17e5f491..c149a67d 100644 --- a/src/main/java/roomescape/theme/domain/repository/ThemeRepository.java +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java @@ -1,4 +1,4 @@ -package roomescape.theme.domain.repository; +package roomescape.theme.infrastructure.persistence; import java.time.LocalDate; import java.util.List; @@ -6,8 +6,6 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import roomescape.theme.domain.Theme; - public interface ThemeRepository extends JpaRepository { @Query(value = """ diff --git a/src/main/java/roomescape/theme/controller/ThemeController.java b/src/main/java/roomescape/theme/web/ThemeController.java similarity index 95% rename from src/main/java/roomescape/theme/controller/ThemeController.java rename to src/main/java/roomescape/theme/web/ThemeController.java index 224190d5..b75758d1 100644 --- a/src/main/java/roomescape/theme/controller/ThemeController.java +++ b/src/main/java/roomescape/theme/web/ThemeController.java @@ -1,4 +1,4 @@ -package roomescape.theme.controller; +package roomescape.theme.web; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -25,10 +25,7 @@ import roomescape.auth.web.support.Admin; import roomescape.auth.web.support.LoginRequired; import roomescape.common.dto.response.RoomescapeApiResponse; import roomescape.common.dto.response.RoomescapeErrorResponse; -import roomescape.theme.dto.ThemeRequest; -import roomescape.theme.dto.ThemeResponse; -import roomescape.theme.dto.ThemesResponse; -import roomescape.theme.service.ThemeService; +import roomescape.theme.business.ThemeService; @RestController @Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") diff --git a/src/main/java/roomescape/theme/dto/ThemeRequest.java b/src/main/java/roomescape/theme/web/ThemeRequest.java similarity index 97% rename from src/main/java/roomescape/theme/dto/ThemeRequest.java rename to src/main/java/roomescape/theme/web/ThemeRequest.java index 438c9645..30725515 100644 --- a/src/main/java/roomescape/theme/dto/ThemeRequest.java +++ b/src/main/java/roomescape/theme/web/ThemeRequest.java @@ -1,4 +1,4 @@ -package roomescape.theme.dto; +package roomescape.theme.web; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/roomescape/theme/dto/ThemeResponse.java b/src/main/java/roomescape/theme/web/ThemeResponse.java similarity index 88% rename from src/main/java/roomescape/theme/dto/ThemeResponse.java rename to src/main/java/roomescape/theme/web/ThemeResponse.java index f95ab31b..fc5711ff 100644 --- a/src/main/java/roomescape/theme/dto/ThemeResponse.java +++ b/src/main/java/roomescape/theme/web/ThemeResponse.java @@ -1,7 +1,7 @@ -package roomescape.theme.dto; +package roomescape.theme.web; import io.swagger.v3.oas.annotations.media.Schema; -import roomescape.theme.domain.Theme; +import roomescape.theme.infrastructure.persistence.Theme; @Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") public record ThemeResponse( diff --git a/src/main/java/roomescape/theme/dto/ThemesResponse.java b/src/main/java/roomescape/theme/web/ThemesResponse.java similarity index 91% rename from src/main/java/roomescape/theme/dto/ThemesResponse.java rename to src/main/java/roomescape/theme/web/ThemesResponse.java index 2ea88862..bf1b9e22 100644 --- a/src/main/java/roomescape/theme/dto/ThemesResponse.java +++ b/src/main/java/roomescape/theme/web/ThemesResponse.java @@ -1,4 +1,4 @@ -package roomescape.theme.dto; +package roomescape.theme.web; import java.util.List; diff --git a/src/test/java/roomescape/payment/business/PaymentServiceTest.java b/src/test/java/roomescape/payment/business/PaymentServiceTest.java index 10a5dbb8..9bb7c79b 100644 --- a/src/test/java/roomescape/payment/business/PaymentServiceTest.java +++ b/src/test/java/roomescape/payment/business/PaymentServiceTest.java @@ -27,8 +27,8 @@ import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) diff --git a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java index a026ee41..5956e6bb 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java @@ -2,7 +2,6 @@ package roomescape.reservation.controller; import static org.assertj.core.api.Assertions.*; import static org.hamcrest.Matchers.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -12,7 +11,6 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Map; -import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -51,8 +49,8 @@ import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.AdminReservationRequest; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) diff --git a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java index 9f809fe9..8a480148 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java @@ -29,8 +29,8 @@ import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) diff --git a/src/test/java/roomescape/reservation/domain/ReservationTest.java b/src/test/java/roomescape/reservation/domain/ReservationTest.java index ba1bc491..a8518ce1 100644 --- a/src/test/java/roomescape/reservation/domain/ReservationTest.java +++ b/src/test/java/roomescape/reservation/domain/ReservationTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.params.provider.MethodSource; import roomescape.member.infrastructure.persistence.Member; import roomescape.member.infrastructure.persistence.Role; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; +import roomescape.theme.infrastructure.persistence.Theme; public class ReservationTest { diff --git a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java index 89dda95c..f8969539 100644 --- a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java +++ b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java @@ -19,8 +19,8 @@ import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @DataJpaTest class ReservationSearchSpecificationTest { diff --git a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java index cd3cb65b..8860d216 100644 --- a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java @@ -27,9 +27,9 @@ import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; -import roomescape.theme.service.ThemeService; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; +import roomescape.theme.business.ThemeService; @SpringBootTest @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) diff --git a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java index 76d4e97a..7d41288b 100644 --- a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java @@ -23,8 +23,8 @@ import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.ReservationTimeRequest; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @Import(ReservationTimeService.class) diff --git a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java index b0f589c3..509182da 100644 --- a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java @@ -28,8 +28,8 @@ import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.response.ReservationResponse; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) diff --git a/src/test/java/roomescape/theme/service/ThemeServiceTest.java b/src/test/java/roomescape/theme/business/ThemeServiceTest.java similarity index 94% rename from src/test/java/roomescape/theme/service/ThemeServiceTest.java rename to src/test/java/roomescape/theme/business/ThemeServiceTest.java index 6a440389..5e42d70c 100644 --- a/src/test/java/roomescape/theme/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/theme/business/ThemeServiceTest.java @@ -1,4 +1,4 @@ -package roomescape.theme.service; +package roomescape.theme.business; import static org.assertj.core.api.Assertions.*; @@ -23,11 +23,11 @@ import roomescape.reservation.dto.response.ReservationTimeResponse; import roomescape.reservation.service.ReservationService; import roomescape.reservation.service.ReservationTimeService; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; -import roomescape.theme.dto.ThemeRequest; -import roomescape.theme.dto.ThemeResponse; -import roomescape.theme.dto.ThemesResponse; +import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeRepository; +import roomescape.theme.web.ThemeRequest; +import roomescape.theme.web.ThemeResponse; +import roomescape.theme.web.ThemesResponse; @DataJpaTest @Import({ReservationTimeService.class, ReservationService.class, MemberService.class, ThemeService.class}) diff --git a/src/test/java/roomescape/theme/controller/ThemeControllerTest.java b/src/test/java/roomescape/theme/web/ThemeControllerTest.java similarity index 99% rename from src/test/java/roomescape/theme/controller/ThemeControllerTest.java rename to src/test/java/roomescape/theme/web/ThemeControllerTest.java index 391ed044..1956bce0 100644 --- a/src/test/java/roomescape/theme/controller/ThemeControllerTest.java +++ b/src/test/java/roomescape/theme/web/ThemeControllerTest.java @@ -1,4 +1,4 @@ -package roomescape.theme.controller; +package roomescape.theme.web; import static org.hamcrest.Matchers.*; diff --git a/src/test/java/roomescape/util/Fixtures.kt b/src/test/java/roomescape/util/Fixtures.kt index abe061ac..bf9bd32d 100644 --- a/src/test/java/roomescape/util/Fixtures.kt +++ b/src/test/java/roomescape/util/Fixtures.kt @@ -11,7 +11,7 @@ import roomescape.payment.web.PaymentCancel import roomescape.reservation.domain.Reservation import roomescape.reservation.domain.ReservationStatus import roomescape.reservation.domain.ReservationTime -import roomescape.theme.domain.Theme +import roomescape.theme.infrastructure.persistence.Theme import java.time.LocalDate import java.time.LocalTime import java.time.OffsetDateTime -- 2.47.2 From 5f57499d79db1a15b61fc09770d4bd12e78b5aac Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 09:28:09 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20theme=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=82=B4=20DTO=20=EC=BD=94=ED=8B=80=EB=A6=B0=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EB=B0=8F=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/theme/web/ThemeDTO.kt | 62 +++++++++++++++++++ .../roomescape/theme/web/ThemeRequest.java | 21 ------- .../roomescape/theme/web/ThemeResponse.java | 21 ------- .../roomescape/theme/web/ThemesResponse.java | 11 ---- 4 files changed, 62 insertions(+), 53 deletions(-) create mode 100644 src/main/java/roomescape/theme/web/ThemeDTO.kt delete mode 100644 src/main/java/roomescape/theme/web/ThemeRequest.java delete mode 100644 src/main/java/roomescape/theme/web/ThemeResponse.java delete mode 100644 src/main/java/roomescape/theme/web/ThemesResponse.java diff --git a/src/main/java/roomescape/theme/web/ThemeDTO.kt b/src/main/java/roomescape/theme/web/ThemeDTO.kt new file mode 100644 index 00000000..14291da9 --- /dev/null +++ b/src/main/java/roomescape/theme/web/ThemeDTO.kt @@ -0,0 +1,62 @@ +package roomescape.theme.web + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size +import roomescape.theme.infrastructure.persistence.Theme + +@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") +@JvmRecord +data class ThemeRequest( + @field:Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") + @NotBlank + @Size(max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.") + val name: String, + + @field:Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") + @NotBlank + @Size(max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.") + val description: String, + + @field:Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") + @NotBlank + val thumbnail: String +) + +@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") +@JvmRecord +data class ThemeResponse( + @field:Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") + val id: Long, + + @field:Schema(description = "테마 이름. 중복을 허용하지 않습니다.") + val name: String, + + @field:Schema(description = "테마 설명") + val description: String, + + @field:Schema(description = "테마 썸네일 이미지 URL") + val thumbnail: String +) { + companion object { + @JvmStatic + fun from(theme: Theme): ThemeResponse { + return ThemeResponse(theme.id, theme.name, theme.description, theme.thumbnail) + } + } +} + +fun Theme.toResponse(): ThemeResponse = ThemeResponse( + id = this.id, + name = this.name, + description = this.description, + thumbnail = this.thumbnail +) + + +@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.") +@JvmRecord +data class ThemesResponse( + @field:Schema(description = "모든 테마 목록") + val themes: List +) diff --git a/src/main/java/roomescape/theme/web/ThemeRequest.java b/src/main/java/roomescape/theme/web/ThemeRequest.java deleted file mode 100644 index 30725515..00000000 --- a/src/main/java/roomescape/theme/web/ThemeRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package roomescape.theme.web; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - -@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") -public record ThemeRequest( - @NotBlank(message = "테마의 이름은 null 또는 공백일 수 없습니다.") - @Size(min = 1, max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.") - @Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") - String name, - @NotBlank(message = "테마의 설명은 null 또는 공백일 수 없습니다.") - @Size(min = 1, max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.") - @Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") - String description, - @NotBlank(message = "테마의 쌈네일은 null 또는 공백일 수 없습니다.") - @Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") - String thumbnail -) { -} diff --git a/src/main/java/roomescape/theme/web/ThemeResponse.java b/src/main/java/roomescape/theme/web/ThemeResponse.java deleted file mode 100644 index fc5711ff..00000000 --- a/src/main/java/roomescape/theme/web/ThemeResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package roomescape.theme.web; - -import io.swagger.v3.oas.annotations.media.Schema; -import roomescape.theme.infrastructure.persistence.Theme; - -@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") -public record ThemeResponse( - @Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") - Long id, - @Schema(description = "테마 이름. 중복을 허용하지 않습니다.") - String name, - @Schema(description = "테마 설명") - String description, - @Schema(description = "테마 썸네일 이미지 URL") - String thumbnail -) { - - public static ThemeResponse from(Theme theme) { - return new ThemeResponse(theme.getId(), theme.getName(), theme.getDescription(), theme.getThumbnail()); - } -} diff --git a/src/main/java/roomescape/theme/web/ThemesResponse.java b/src/main/java/roomescape/theme/web/ThemesResponse.java deleted file mode 100644 index bf1b9e22..00000000 --- a/src/main/java/roomescape/theme/web/ThemesResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.theme.web; - -import java.util.List; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.") -public record ThemesResponse( - @Schema(description = "모든 테마 목록") List themes -) { -} -- 2.47.2 From 6f767c39fd49e81a30fc43592b8377c584556acf Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:17:31 +0900 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20AdminInterceptor=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=EC=8B=9C=20=EB=B0=98=ED=99=98=EB=90=98=EB=8A=94=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=83=80=EC=9E=85=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/auth/web/support/AuthInterceptors.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt b/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt index f63f4645..20b5b53d 100644 --- a/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt +++ b/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt @@ -69,9 +69,9 @@ class AdminInterceptor( val token: String? = request.accessTokenCookie().value val memberId: Long = jwtHandler.getMemberIdFromToken(token) member = memberService.findById(memberId) - } catch (e: RoomescapeException) { + } catch (_: RoomescapeException) { response.sendRedirect("/login") - throw e + throw RoomescapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN) } with(member) { -- 2.47.2 From d6be6ba24029396228cb89775cc474debc36c31a Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:18:26 +0900 Subject: [PATCH 04/17] Rename .java to .kt --- .../theme/web/{ThemeController.java => ThemeController.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/roomescape/theme/web/{ThemeController.java => ThemeController.kt} (100%) diff --git a/src/main/java/roomescape/theme/web/ThemeController.java b/src/main/java/roomescape/theme/web/ThemeController.kt similarity index 100% rename from src/main/java/roomescape/theme/web/ThemeController.java rename to src/main/java/roomescape/theme/web/ThemeController.kt -- 2.47.2 From 0896e3bf30dfd5f16e06129fa792c6ac83b46df1 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:18:26 +0900 Subject: [PATCH 05/17] =?UTF-8?q?refactor:=20ThemeController=20=EC=BD=94?= =?UTF-8?q?=ED=8B=80=EB=A6=B0=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B0=8F=20Swagger=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/theme/docs/ThemeAPI.kt | 51 +++++++ .../roomescape/theme/web/ThemeController.kt | 125 ++++++------------ .../java/roomescape/theme/web/ThemeDTO.kt | 2 + 3 files changed, 92 insertions(+), 86 deletions(-) create mode 100644 src/main/java/roomescape/theme/docs/ThemeAPI.kt diff --git a/src/main/java/roomescape/theme/docs/ThemeAPI.kt b/src/main/java/roomescape/theme/docs/ThemeAPI.kt new file mode 100644 index 00000000..624d830a --- /dev/null +++ b/src/main/java/roomescape/theme/docs/ThemeAPI.kt @@ -0,0 +1,51 @@ +package roomescape.theme.docs + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import roomescape.auth.web.support.Admin +import roomescape.auth.web.support.LoginRequired +import roomescape.common.dto.response.CommonApiResponse +import roomescape.theme.web.ThemeRequest +import roomescape.theme.web.ThemeResponse +import roomescape.theme.web.ThemesResponse + +@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") +interface ThemeAPI { + + @LoginRequired + @Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun getAllThemes(): ResponseEntity> + + @Operation(summary = "가장 많이 예약된 테마 조회") + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun getMostReservedThemes( + @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int + ): ResponseEntity> + + @Admin + @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), + ) + fun saveTheme( + @Valid @RequestBody request: ThemeRequest, + ): ResponseEntity> + + @Admin + @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true), + ) + fun removeTheme( + @PathVariable id: Long + ): ResponseEntity> +} diff --git a/src/main/java/roomescape/theme/web/ThemeController.kt b/src/main/java/roomescape/theme/web/ThemeController.kt index b75758d1..c7182b83 100644 --- a/src/main/java/roomescape/theme/web/ThemeController.kt +++ b/src/main/java/roomescape/theme/web/ThemeController.kt @@ -1,98 +1,51 @@ -package roomescape.theme.web; +package roomescape.theme.web -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -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.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import roomescape.auth.web.support.Admin; -import roomescape.auth.web.support.LoginRequired; -import roomescape.common.dto.response.RoomescapeApiResponse; -import roomescape.common.dto.response.RoomescapeErrorResponse; -import roomescape.theme.business.ThemeService; +import io.swagger.v3.oas.annotations.Parameter +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import roomescape.common.dto.response.CommonApiResponse +import roomescape.theme.business.ThemeService +import roomescape.theme.docs.ThemeAPI +import java.net.URI @RestController -@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") -public class ThemeController { +class ThemeController( + private val themeService: ThemeService +) : ThemeAPI { - private final ThemeService themeService; + @GetMapping("/themes") + override fun getAllThemes(): ResponseEntity> { + val response: ThemesResponse = themeService.findAllThemes() - public ThemeController(ThemeService themeService) { - this.themeService = themeService; - } + return ResponseEntity.ok(CommonApiResponse(response)) + } - @LoginRequired - @GetMapping("/themes") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = "로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) - }) - public RoomescapeApiResponse getAllThemes() { - return RoomescapeApiResponse.success(themeService.findAllThemes()); - } + @GetMapping("/themes/most-reserved-last-week") + override fun getMostReservedThemes( + @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int + ): ResponseEntity> { + val response: ThemesResponse = themeService.getMostReservedThemesByCount(count) - @GetMapping("/themes/most-reserved-last-week") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "가장 많이 예약된 테마 조회") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) - }) - public RoomescapeApiResponse getMostReservedThemes( - @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") int count - ) { - return RoomescapeApiResponse.success(themeService.getMostReservedThemesByCount(count)); - } + return ResponseEntity.ok(CommonApiResponse(response)) + } - @Admin - @PostMapping("/themes") - @ResponseStatus(HttpStatus.CREATED) - @Operation(summary = "테마 추가", tags = "관리자 로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), - @ApiResponse(responseCode = "409", description = "같은 이름의 테마를 추가할 수 없습니다.", - content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) - }) - public RoomescapeApiResponse saveTheme( - @Valid @RequestBody ThemeRequest request, - HttpServletResponse response - ) { - ThemeResponse themeResponse = themeService.addTheme(request); - response.setHeader(HttpHeaders.LOCATION, "/themes/" + themeResponse.id()); + @PostMapping("/themes") + override fun saveTheme( + @RequestBody @Valid request: ThemeRequest + ): ResponseEntity> { + val themeResponse: ThemeResponse = themeService.addTheme(request) - return RoomescapeApiResponse.success(themeResponse); - } + return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) + .body(CommonApiResponse(themeResponse)) + } - @Admin - @DeleteMapping("/themes/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - @Operation(summary = "테마 삭제", tags = "관리자 로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true), - @ApiResponse(responseCode = "409", description = "예약된 테마는 삭제할 수 없습니다.", - content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) - }) - public RoomescapeApiResponse removeTheme( - @NotNull(message = "themeId는 null일 수 없습니다.") @PathVariable Long id - ) { - themeService.removeThemeById(id); + @DeleteMapping("/themes/{id}") + override fun removeTheme( + @PathVariable id: Long + ): ResponseEntity> { + themeService.removeThemeById(id) - return RoomescapeApiResponse.success(); - } + return ResponseEntity.noContent().build() + } } diff --git a/src/main/java/roomescape/theme/web/ThemeDTO.kt b/src/main/java/roomescape/theme/web/ThemeDTO.kt index 14291da9..7f9a4ad8 100644 --- a/src/main/java/roomescape/theme/web/ThemeDTO.kt +++ b/src/main/java/roomescape/theme/web/ThemeDTO.kt @@ -3,6 +3,7 @@ package roomescape.theme.web import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size +import org.hibernate.validator.constraints.URL import roomescape.theme.infrastructure.persistence.Theme @Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") @@ -20,6 +21,7 @@ data class ThemeRequest( @field:Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") @NotBlank + @URL val thumbnail: String ) -- 2.47.2 From f56954536129876c935ed637da49454d3805ac3e Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:19:28 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20RoomescapeApiTest=20=EB=82=B4=20D?= =?UTF-8?q?elete=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=EC=9D=BD=EC=96=B4?= =?UTF-8?q?=EC=98=A4=EB=8A=94=20=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/util/RoomescapeApiTest.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/test/java/roomescape/util/RoomescapeApiTest.kt b/src/test/java/roomescape/util/RoomescapeApiTest.kt index 23addfe6..803bf86e 100644 --- a/src/test/java/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/java/roomescape/util/RoomescapeApiTest.kt @@ -55,9 +55,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() { header(HttpHeaders.COOKIE, "accessToken=token") }.apply { log.takeIf { it }?.let { this.andDo { print() } } - }.andExpect { - assert - } + }.andExpect(assert) fun runPostTest( mockMvc: MockMvc, @@ -77,6 +75,19 @@ abstract class RoomescapeApiTest : BehaviorSpec() { assert } + fun runDeleteTest( + mockMvc: MockMvc, + endpoint: String, + log: Boolean = false, + assert: MockMvcResultMatchersDsl.() -> Unit + ): ResultActionsDsl = mockMvc.delete(endpoint) { + header(HttpHeaders.COOKIE, "accessToken=token") + }.apply { + log.takeIf { it }?.let { this.andDo { print() } } + }.andExpect { + assert + } + fun loginAsAdmin() { every { jwtHandler.getMemberIdFromToken(any()) @@ -104,6 +115,13 @@ abstract class RoomescapeApiTest : BehaviorSpec() { every { memberRepository.findByIdOrNull(NOT_LOGGED_IN_USERID) } returns null } + fun MvcResult.readValue(valueType: Class): T = this.response.contentAsString + .takeIf { it.isNotBlank() } + ?.let { readValue(it, valueType) } + ?: throw RuntimeException(""" + [Test] Exception occurred while reading response json: ${this.response.contentAsString} with value type: $valueType + """.trimIndent()) + fun readValue(responseJson: String, valueType: Class): T = objectMapper .readTree(responseJson)["data"] ?.let { objectMapper.convertValue(it, valueType) } -- 2.47.2 From e943fbe0df4bb32465dbcc5923938e52a15191ae Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:20:55 +0900 Subject: [PATCH 07/17] Rename .java to .kt --- .../web/{ThemeControllerTest.java => ThemeControllerTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/java/roomescape/theme/web/{ThemeControllerTest.java => ThemeControllerTest.kt} (100%) diff --git a/src/test/java/roomescape/theme/web/ThemeControllerTest.java b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt similarity index 100% rename from src/test/java/roomescape/theme/web/ThemeControllerTest.java rename to src/test/java/roomescape/theme/web/ThemeControllerTest.kt -- 2.47.2 From 9d145f7f664b08e5943c413cff5bccca77ec819a Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:20:55 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20ThemeControllerTest=20=EC=BD=94?= =?UTF-8?q?=ED=8B=80=EB=A6=B0=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체적으로는 MockMvc를 이용하였으나 mocking이 애매한 API는 MostReservedThemeAPITest에서 실제 동작을 검증함. --- .../theme/util/TestThemeCreateUtil.kt | 44 ++ .../theme/web/MostReservedThemeAPITest.kt | 112 +++++ .../theme/web/ThemeControllerTest.kt | 441 +++++++++++------- 3 files changed, 438 insertions(+), 159 deletions(-) create mode 100644 src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt create mode 100644 src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt diff --git a/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt new file mode 100644 index 00000000..daebd118 --- /dev/null +++ b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt @@ -0,0 +1,44 @@ +package roomescape.theme.util + +import jakarta.persistence.EntityManager +import roomescape.member.infrastructure.persistence.Member +import roomescape.reservation.domain.ReservationStatus +import roomescape.reservation.domain.ReservationTime +import roomescape.theme.infrastructure.persistence.Theme +import roomescape.util.MemberFixture +import roomescape.util.ReservationFixture +import roomescape.util.ReservationTimeFixture +import roomescape.util.ThemeFixture +import java.time.LocalDate +import java.time.LocalTime + +object TestThemeCreateUtil { + fun createThemeWithReservations( + entityManager: EntityManager, + name: String, + reservedCount: Int, + date: LocalDate, + ): Long { + val theme: Theme = ThemeFixture.create(name = name).also { entityManager.persist(it) } + val member: Member = MemberFixture.create().also { entityManager.persist(it) } + + for (i in 1..reservedCount) { + val time: ReservationTime = ReservationTimeFixture.create( + startAt = LocalTime.now().plusMinutes(i.toLong()) + ).also { entityManager.persist(it) } + + ReservationFixture.create( + date = date, + theme = theme, + member = member, + reservationTime = time, + status = ReservationStatus.CONFIRMED + ).also { entityManager.persist(it) } + } + + entityManager.flush() + entityManager.clear() + + return theme.id + } +} diff --git a/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt new file mode 100644 index 00000000..f050cfaf --- /dev/null +++ b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt @@ -0,0 +1,112 @@ +package roomescape.theme.web + +import io.kotest.core.spec.style.FunSpec +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import jakarta.persistence.EntityManager +import org.hamcrest.Matchers.equalTo +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.transaction.support.TransactionTemplate +import roomescape.theme.business.ThemeService +import roomescape.theme.util.TestThemeCreateUtil +import java.time.LocalDate +import kotlin.random.Random + +/** + * GET /themes/most-reserved-last-week API 테스트 + * 상세 테스트는 Repository 테스트에서 진행 + * 날짜 범위, 예약 수만 검증 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MostReservedThemeAPITest( + @LocalServerPort val port: Int, + val themeService: ThemeService, + val transactionTemplate: TransactionTemplate, + val entityManager: EntityManager, +) : FunSpec() { + + init { + beforeSpec { + transactionTemplate.executeWithoutResult { + // 지난 7일간 예약된 테마 10개 생성 + for (i in 1..10) { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마$i", + reservedCount = 1, + date = LocalDate.now().minusDays(Random.nextLong(1, 7)) + ) + } + + // 8일 전 예약된 테마 1개 생성 + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마11", + reservedCount = 1, + date = LocalDate.now().minusDays(8) + ) + } + } + + context("가장 많이 예약된 테마를 조회할 때,") { + val endpoint = "/themes/most-reserved-last-week" + test("갯수를 입력하지 않으면 10개를 반환한다.") { + Given { + port(port) + } When { + get(endpoint) + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(10)) + } + } + + test("입력된 갯수가 조회된 갯수보다 크면 조회된 갯수만큼 반환한다.") { + val count = 15 + Given { + port(port) + } When { + param("count", count) + get("/themes/most-reserved-last-week") + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(10)) + } + } + + test("입력된 갯수가 조회된 갯수보다 작으면 입력된 갯수만큼 반환한다.") { + val count = 5 + Given { + port(port) + } When { + param("count", count) + get("/themes/most-reserved-last-week") + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(count)) + } + } + + test("7일 전 부터 1일 전 까지 예약된 테마를 대상으로 한다.") { + // 현재 저장된 데이터는 지난 7일간 예약된 테마 10개와 8일 전 예약된 테마 1개 + // 8일 전 예약된 테마는 제외되어야 하므로, 10개가 조회되어야 한다. + val count = 11 + Given { + port(port) + } When { + param("count", count) + get("/themes/most-reserved-last-week") + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(10)) + } + } + } + } +} diff --git a/src/test/java/roomescape/theme/web/ThemeControllerTest.kt b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt index 1956bce0..af3ea616 100644 --- a/src/test/java/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt @@ -1,184 +1,307 @@ -package roomescape.theme.web; +package roomescape.theme.web -import static org.hamcrest.Matchers.*; +import com.ninjasquad.springmockk.MockkBean +import com.ninjasquad.springmockk.SpykBean +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.just +import io.mockk.runs +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import roomescape.theme.business.ThemeService +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.util.RoomescapeApiTest +import roomescape.util.ThemeFixture -import java.util.Map; -import java.util.stream.Stream; +@WebMvcTest(ThemeController::class) +class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.Sql.ExecutionPhase; + @SpykBean + private lateinit var themeService: ThemeService -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.restassured.http.Header; -import roomescape.member.infrastructure.persistence.Member; -import roomescape.member.infrastructure.persistence.MemberRepository; -import roomescape.member.infrastructure.persistence.Role; + @MockkBean + private lateinit var themeRepository: ThemeRepository -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) -class ThemeControllerTest { + init { + Given("모든 테마를 조회할 때") { + val endpoint = "/themes" - @LocalServerPort - private int port; + When("로그인 상태가 아니라면") { + doNotLogin() - @Autowired - private MemberRepository memberRepository; + Then("로그인 페이지로 이동한다.") { + runGetTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + header { + string("Location", "/login") + } + } + } + } - @Test - @DisplayName("모든 테마 정보를 조회한다.") - void readThemes() { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); + When("로그인 상태라면") { + loginAsUser() - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .when().get("/themes") - .then().log().all() - .statusCode(200) - .body("data.themes.size()", is(0)); - } + Then("조회에 성공한다.") { + every { + themeRepository.findAll() + } returns listOf( + ThemeFixture.create(id = 1, name = "theme1"), + ThemeFixture.create(id = 2, name = "theme2"), + ThemeFixture.create(id = 3, name = "theme3") + ) - @Test - @DisplayName("테마를 추가한다.") - void createThemes() { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); + val response: ThemesResponse = runGetTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isOk() } + content { + contentType(MediaType.APPLICATION_JSON) + } + }.andReturn().readValue(ThemesResponse::class.java) - Map params = Map.of( - "name", "테마명", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ); + assertSoftly(response.themes) { + it.size shouldBe 3 + it.map { m -> m.name } shouldContainAll listOf("theme1", "theme2", "theme3") + } + } + } + } - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(params) - .when().post("/themes") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/themes/1"); - } + Given("테마를 추가할 때") { + val endpoint = "/themes" + val request = ThemeRequest( + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" + ) - @Test - @DisplayName("테마를 삭제한다.") - void deleteThemes() { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); + When("로그인 상태가 아니라면") { + doNotLogin() + Then("로그인 페이지로 이동한다.") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { is3xxRedirection() } + header { + string("Location", "/login") + } + } + } + } - Map params = Map.of( - "name", "테마명", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ); + When("관리자가 아닌 회원은") { + loginAsUser() + Then("로그인 페이지로 이동한다.") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { is3xxRedirection() } + jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } + } + } + } - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(params) - .when().post("/themes") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/themes/1"); + When("동일한 이름의 테마가 있으면") { + loginAsAdmin() - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .when().delete("/themes/1") - .then().log().all() - .statusCode(204); - } + Then("409 에러를 응답한다.") { + every { + themeRepository.existsByName(request.name) + } returns true - /* - * reservationData DataSet ThemeID 별 reservation 개수 - * 5,4,2,5,2,3,1,1,1,1,1 - * 예약 수 내림차순 + ThemeId 오름차순 정렬 순서 - * 1, 4, 2, 6, 3, 5, 7, 8, 9, 10 - */ - @Test - @DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.") - @Sql(scripts = {"/truncate.sql", "/reservationData.sql"}, executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) - void readTop10ThemesDescOrder() { - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .when().get("/themes/most-reserved-last-week?count=10") - .then().log().all() - .statusCode(200) - .body("data.themes.size()", is(10)) - .body("data.themes.id", contains(1, 4, 2, 6, 3, 5, 7, 8, 9, 10)); - } + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isConflict() } + jsonPath("$.errorType") { value("THEME_DUPLICATED") } + } + } + } - @ParameterizedTest - @MethodSource("requestValidateSource") - @DisplayName("테마 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.") - void validateBlankRequest(Map invalidRequestBody) { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); + When("값이 잘못 입력되면 400 에러를 응답한다") { + beforeTest { + loginAsAdmin() + } - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(invalidRequestBody) - .when().post("/themes") - .then().log().all() - .statusCode(400); - } + val request = ThemeRequest( + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" + ) - static Stream> requestValidateSource() { - return Stream.of( - Map.of( - "name", "테마명", - "thumbnail", "http://testsfasdgasd.com" - ), - Map.of( - "name", "", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ), - Map.of( - "name", " ", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ) - ); - } + fun runTest(request: ThemeRequest) { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isBadRequest() } + } + } - private String getAdminAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN)); + Then("이름이 공백인 경우") { + val invalidRequest = request.copy(name = " ") + runTest(invalidRequest) + } - Map loginParams = Map.of( - "email", email, - "password", password - ); + Then("이름이 20글자를 초과하는 경우") { + val invalidRequest = request.copy(name = "a".repeat(21)) + runTest(invalidRequest) + } - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); + Then("설명이 공백인 경우") { + val invalidRequest = request.copy(description = " ") + runTest(invalidRequest) + } - return "accessToken=" + accessToken; - } + Then("설명이 100글자를 초과하는 경우") { + val invalidRequest = request.copy(description = "a".repeat(101)) + runTest(invalidRequest) + } + + Then("썸네일이 공백인 경우") { + val invalidRequest = request.copy(thumbnail = " ") + runTest(invalidRequest) + } + + Then("썸네일이 URL 형식이 아닌 경우") { + val invalidRequest = request.copy(thumbnail = "invalid-url") + runTest(invalidRequest) + } + } + + When("저장에 성공하면") { + loginAsAdmin() + + val theme = ThemeFixture.create( + id = 1, + name = request.name, + description = request.description, + thumbnail = request.thumbnail + ) + + every { + themeRepository.existsByName(request.name) + } returns false + + every { + themeRepository.save(any()) + } returns theme + + Then("201 응답을 받는다.") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isCreated() } + header { + string("Location", "/themes/${theme.id}") + } + jsonPath("$.data.id") { value(theme.id) } + jsonPath("$.data.name") { value(theme.name) } + jsonPath("$.data.description") { value(theme.description) } + jsonPath("$.data.thumbnail") { value(theme.thumbnail) } + } + } + } + } + + Given("테마를 제거할 때") { + val themeId = 1L + val endpoint = "/themes/$themeId" + + When("로그인 상태가 아니라면") { + doNotLogin() + Then("로그인 페이지로 이동한다.") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + header { + string("Location", "/login") + } + } + } + } + + When("관리자가 아닌 회원은") { + loginAsUser() + Then("로그인 페이지로 이동한다.") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } + } + } + } + + When("입력된 ID에 해당하는 테마가 없으면") { + loginAsAdmin() + + Then("409 에러를 응답한다.") { + every { + themeRepository.isReservedTheme(themeId) + } returns true + + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isConflict() } + jsonPath("$.errorType") { value("THEME_IS_USED_CONFLICT") } + } + } + } + + When("정상적으로 제거되면") { + loginAsAdmin() + + every { + themeRepository.isReservedTheme(themeId) + } returns false + + every { + themeRepository.deleteById(themeId) + } just runs + + Then("204 응답을 받는다.") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isNoContent() } + } + } + } + } + } } -- 2.47.2 From b6dd89c9d45d926b507ed6540260f2d6c8581ffc Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:24:59 +0900 Subject: [PATCH 09/17] Rename .java to .kt --- .../infrastructure/persistence/{Theme.java => ThemeEntity.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/roomescape/theme/infrastructure/persistence/{Theme.java => ThemeEntity.kt} (100%) diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/Theme.java b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt similarity index 100% rename from src/main/java/roomescape/theme/infrastructure/persistence/Theme.java rename to src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt -- 2.47.2 From c9ab0aff9534272e07cf81f18c7161f27114cbf7 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 18:24:59 +0900 Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20Theme=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=94=ED=8B=80=EB=A6=B0=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=B0=8F=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD(->=20ThemeEntity)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/domain/Reservation.java | 12 ++-- .../service/ReservationService.java | 4 +- .../theme/business/ThemeService.java | 6 +- .../infrastructure/persistence/ThemeEntity.kt | 72 +++---------------- .../persistence/ThemeRepository.java | 6 +- .../java/roomescape/theme/web/ThemeDTO.kt | 14 ++-- .../payment/business/PaymentServiceTest.java | 8 +-- .../controller/ReservationControllerTest.java | 30 ++++---- .../ReservationTimeControllerTest.java | 4 +- .../reservation/domain/ReservationTest.java | 10 +-- .../ReservationSearchSpecificationTest.java | 4 +- .../service/ReservationServiceTest.java | 18 ++--- .../service/ReservationTimeServiceTest.java | 4 +- .../ReservationWithPaymentServiceTest.java | 10 +-- .../theme/business/ThemeServiceTest.java | 18 ++--- .../theme/util/TestThemeCreateUtil.kt | 10 +-- src/test/java/roomescape/util/Fixtures.kt | 8 +-- 17 files changed, 96 insertions(+), 142 deletions(-) diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java index 0cb0b972..6509596f 100644 --- a/src/main/java/roomescape/reservation/domain/Reservation.java +++ b/src/main/java/roomescape/reservation/domain/Reservation.java @@ -18,7 +18,7 @@ import jakarta.persistence.ManyToOne; import roomescape.common.exception.ErrorType; import roomescape.common.exception.RoomescapeException; import roomescape.member.infrastructure.persistence.Member; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; @Entity public class Reservation { @@ -35,7 +35,7 @@ public class Reservation { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "theme_id", nullable = false) - private Theme theme; + private ThemeEntity theme; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) @@ -50,7 +50,7 @@ public class Reservation { public Reservation( LocalDate date, ReservationTime reservationTime, - Theme theme, + ThemeEntity theme, Member member, ReservationStatus status ) { @@ -61,7 +61,7 @@ public class Reservation { Long id, LocalDate date, ReservationTime reservationTime, - Theme theme, + ThemeEntity theme, Member member, ReservationStatus status ) { @@ -74,7 +74,7 @@ public class Reservation { this.reservationStatus = status; } - private void validateIsNull(LocalDate date, ReservationTime reservationTime, Theme theme, Member member, + private void validateIsNull(LocalDate date, ReservationTime reservationTime, ThemeEntity theme, Member member, ReservationStatus reservationStatus) { if (date == null || reservationTime == null || theme == null || member == null || reservationStatus == null) { throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this), @@ -98,7 +98,7 @@ public class Reservation { return reservationTime; } - public Theme getTheme() { + public ThemeEntity getTheme() { return theme; } diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java index 17e228a0..0a3098ac 100644 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -24,7 +24,7 @@ import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.MyReservationsResponse; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationsResponse; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.business.ThemeService; @Service @@ -146,7 +146,7 @@ public class ReservationService { private Reservation getReservationForSave(Long timeId, Long themeId, LocalDate date, Long memberId, ReservationStatus status) { ReservationTime time = reservationTimeService.findTimeById(timeId); - Theme theme = themeService.findThemeById(themeId); + ThemeEntity theme = themeService.findThemeById(themeId); Member member = memberService.findById(memberId); validateDateAndTime(date, time); diff --git a/src/main/java/roomescape/theme/business/ThemeService.java b/src/main/java/roomescape/theme/business/ThemeService.java index 0b46b3dc..e900752d 100644 --- a/src/main/java/roomescape/theme/business/ThemeService.java +++ b/src/main/java/roomescape/theme/business/ThemeService.java @@ -9,7 +9,7 @@ import org.springframework.transaction.annotation.Transactional; import roomescape.common.exception.ErrorType; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; import roomescape.theme.web.ThemeRequest; import roomescape.theme.web.ThemeResponse; @@ -26,7 +26,7 @@ public class ThemeService { } @Transactional(readOnly = true) - public Theme findThemeById(Long id) { + public ThemeEntity findThemeById(Long id) { return themeRepository.findById(id) .orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND, String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST)); @@ -59,7 +59,7 @@ public class ThemeService { public ThemeResponse addTheme(ThemeRequest request) { validateIsSameThemeNameExist(request.name()); - Theme theme = themeRepository.save(new Theme(request.name(), request.description(), request.thumbnail())); + ThemeEntity theme = themeRepository.save(new ThemeEntity(request.name(), request.description(), request.thumbnail())); return ThemeResponse.from(theme); } diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt index 9f5f64c8..6d8e3d79 100644 --- a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt @@ -1,65 +1,15 @@ -package roomescape.theme.infrastructure.persistence; +package roomescape.theme.infrastructure.persistence -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.* @Entity -public class Theme { +@Table(name = "theme") +class ThemeEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private String description; - - private String thumbnail; - - protected Theme() { - } - - public Theme(String name, String description, String thumbnail) { - this(null, name, description, thumbnail); - } - - public Theme( - Long id, - String name, - String description, - String thumbnail - ) { - this.id = id; - this.name = name; - this.description = description; - this.thumbnail = thumbnail; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getThumbnail() { - return thumbnail; - } - - @Override - public String toString() { - return "Theme{" + - "id=" + id + - ", name=" + name + - ", description=" + description + - ", thumbnail=" + thumbnail + - '}'; - } -} + var name: String, + var description: String, + var thumbnail: String +) \ No newline at end of file diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java index c149a67d..0a48af56 100644 --- a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java @@ -6,18 +6,18 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -public interface ThemeRepository extends JpaRepository { +public interface ThemeRepository extends JpaRepository { @Query(value = """ SELECT t - FROM Theme t + FROM ThemeEntity t RIGHT JOIN Reservation r ON t.id = r.theme.id WHERE r.date BETWEEN :startDate AND :endDate GROUP BY r.theme.id ORDER BY COUNT(r.theme.id) DESC, t.id ASC LIMIT :limit """) - List findTopNThemeBetweenStartDateAndEndDate(LocalDate startDate, LocalDate endDate, int limit); + List findTopNThemeBetweenStartDateAndEndDate(LocalDate startDate, LocalDate endDate, int limit); boolean existsByName(String name); diff --git a/src/main/java/roomescape/theme/web/ThemeDTO.kt b/src/main/java/roomescape/theme/web/ThemeDTO.kt index 7f9a4ad8..27c4ea61 100644 --- a/src/main/java/roomescape/theme/web/ThemeDTO.kt +++ b/src/main/java/roomescape/theme/web/ThemeDTO.kt @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size import org.hibernate.validator.constraints.URL -import roomescape.theme.infrastructure.persistence.Theme +import roomescape.theme.infrastructure.persistence.ThemeEntity @Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") @JvmRecord @@ -42,14 +42,14 @@ data class ThemeResponse( ) { companion object { @JvmStatic - fun from(theme: Theme): ThemeResponse { - return ThemeResponse(theme.id, theme.name, theme.description, theme.thumbnail) + fun from(themeEntity: ThemeEntity): ThemeResponse { + return ThemeResponse(themeEntity.id!!, themeEntity.name, themeEntity.description, themeEntity.thumbnail) } } } -fun Theme.toResponse(): ThemeResponse = ThemeResponse( - id = this.id, +fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse( + id = this.id!!, name = this.name, description = this.description, thumbnail = this.thumbnail @@ -62,3 +62,7 @@ data class ThemesResponse( @field:Schema(description = "모든 테마 목록") val themes: List ) + +fun List.toResponse(): ThemesResponse = ThemesResponse( + themes = this.map { it.toResponse()} +) diff --git a/src/test/java/roomescape/payment/business/PaymentServiceTest.java b/src/test/java/roomescape/payment/business/PaymentServiceTest.java index 9bb7c79b..57f057a8 100644 --- a/src/test/java/roomescape/payment/business/PaymentServiceTest.java +++ b/src/test/java/roomescape/payment/business/PaymentServiceTest.java @@ -27,7 +27,7 @@ import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @@ -57,7 +57,7 @@ class PaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -79,7 +79,7 @@ class PaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -116,7 +116,7 @@ class PaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); diff --git a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java index 5956e6bb..5a888a55 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java @@ -49,7 +49,7 @@ import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.AdminReservationRequest; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -89,7 +89,7 @@ public class ReservationControllerTest { LocalDate date = LocalDate.now().plusDays(1L); reservationTimeRepository.save(new ReservationTime(time)); - themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Map reservationParams = Map.of( "date", date.toString(), @@ -124,7 +124,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member1 = memberRepository.save(new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER)); // when @@ -152,7 +152,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member waitingMember = memberRepository.save( new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER)); @@ -179,7 +179,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when @@ -208,7 +208,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Reservation reservation = reservationRepository.save( new Reservation(LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED)); @@ -228,7 +228,7 @@ public class ReservationControllerTest { String adminTokenCookie = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member confirmedMember = memberRepository.save( new Member(null, "name1", "email@email.com", "password", Role.MEMBER)); Member waitingMember = memberRepository.save( @@ -256,7 +256,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member anotherMember = memberRepository.save( new Member(null, "name", "email@email.com", "password", Role.MEMBER)); @@ -337,7 +337,7 @@ public class ReservationControllerTest { void getAllReservations(String requestURI, String responseFieldName, int expectedSize) { // given LocalDate date = LocalDate.now().plusDays(1); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); ReservationTime time1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30))); ReservationTime time2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(19, 30))); @@ -366,7 +366,7 @@ public class ReservationControllerTest { void removeNotPaidReservation() { // given LocalDate date = LocalDate.now().plusDays(1); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); @@ -390,7 +390,7 @@ public class ReservationControllerTest { // given String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); LocalDate date = LocalDate.now().plusDays(1); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); @@ -421,7 +421,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().minusHours(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); @@ -514,7 +514,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); @@ -540,7 +540,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); @@ -585,7 +585,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String adminAccessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); diff --git a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java index 8a480148..baaa53a3 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java @@ -29,7 +29,7 @@ import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -224,7 +224,7 @@ public class ReservationTimeControllerTest { ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 0))); ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30))); - Theme theme = themeRepository.save(new Theme("테마명1", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명1", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); reservationRepository.save( diff --git a/src/test/java/roomescape/reservation/domain/ReservationTest.java b/src/test/java/roomescape/reservation/domain/ReservationTest.java index a8518ce1..6f19375d 100644 --- a/src/test/java/roomescape/reservation/domain/ReservationTest.java +++ b/src/test/java/roomescape/reservation/domain/ReservationTest.java @@ -13,14 +13,14 @@ import org.junit.jupiter.params.provider.MethodSource; import roomescape.member.infrastructure.persistence.Member; import roomescape.member.infrastructure.persistence.Role; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; public class ReservationTest { @ParameterizedTest @MethodSource("validateConstructorParameterBlankSource") @DisplayName("객체 생성 시, null 또는 공백이 존재하면 예외를 발생한다.") - void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, Theme theme, + void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, ThemeEntity theme, Member member) { // when & then @@ -33,12 +33,12 @@ public class ReservationTest { return Stream.of( Arguments.of(null, new ReservationTime(LocalTime.now().plusHours(1)), - new Theme("테마명", "설명", "썸네일URI"), + new ThemeEntity(null, "테마명", "설명", "썸네일URI"), new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), null, - new Theme("테마명", "설명", "썸네일URI"), + new ThemeEntity(null, "테마명", "설명", "썸네일URI"), new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), @@ -48,7 +48,7 @@ public class ReservationTest { Arguments.of( LocalDate.now(), new ReservationTime(LocalTime.now().plusHours(1)), - new Theme("테마명", "설명", "썸네일URI"), + new ThemeEntity(null, "테마명", "설명", "썸네일URI"), null) ); } diff --git a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java index f8969539..56f33293 100644 --- a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java +++ b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java @@ -19,7 +19,7 @@ import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; @DataJpaTest @@ -52,7 +52,7 @@ class ReservationSearchSpecificationTest { LocalDateTime dateTime = LocalDateTime.now(); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); ReservationTime time = timeRepository.save(new ReservationTime(dateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "description", "thumbnail")); reservation1 = reservationRepository.save( new Reservation(dateTime.toLocalDate(), time, theme, member, ReservationStatus.CONFIRMED)); diff --git a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java index 8860d216..b85aa741 100644 --- a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java @@ -27,7 +27,7 @@ import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; import roomescape.theme.business.ThemeService; @@ -52,7 +52,7 @@ class ReservationServiceTest { void reservationAlreadyExistFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member1 = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member2 = memberRepository.save(new Member(null, "name2", "email2@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); @@ -74,7 +74,7 @@ class ReservationServiceTest { void requestWaitWhenAlreadyReserveFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); @@ -94,7 +94,7 @@ class ReservationServiceTest { void requestWaitTwiceFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); @@ -118,7 +118,7 @@ class ReservationServiceTest { void beforeDateReservationFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); LocalDate beforeDate = LocalDate.now().minusDays(1L); @@ -135,7 +135,7 @@ class ReservationServiceTest { // given LocalDateTime beforeTime = LocalDateTime.now().minusHours(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when & then @@ -151,7 +151,7 @@ class ReservationServiceTest { // given LocalDateTime beforeTime = LocalDateTime.now().minusDays(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Long NotExistMemberId = 1L; // when & then @@ -179,7 +179,7 @@ class ReservationServiceTest { void confirmWaitingWhenReservationExist() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN)); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); @@ -202,7 +202,7 @@ class ReservationServiceTest { void approveWaiting() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN)); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); diff --git a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java index 7d41288b..473f5698 100644 --- a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java @@ -23,7 +23,7 @@ import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.ReservationTimeRequest; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @@ -74,7 +74,7 @@ class ReservationTimeServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save( new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when diff --git a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java index 509182da..4a7e5cdc 100644 --- a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java @@ -28,7 +28,7 @@ import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.response.ReservationResponse; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @@ -60,7 +60,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); @@ -96,7 +96,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); @@ -124,7 +124,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation saved = reservationRepository.save( new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)); @@ -146,7 +146,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); diff --git a/src/test/java/roomescape/theme/business/ThemeServiceTest.java b/src/test/java/roomescape/theme/business/ThemeServiceTest.java index 5e42d70c..3df47809 100644 --- a/src/test/java/roomescape/theme/business/ThemeServiceTest.java +++ b/src/test/java/roomescape/theme/business/ThemeServiceTest.java @@ -23,7 +23,7 @@ import roomescape.reservation.dto.response.ReservationTimeResponse; import roomescape.reservation.service.ReservationService; import roomescape.reservation.service.ReservationTimeService; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.infrastructure.persistence.ThemeRepository; import roomescape.theme.web.ThemeRequest; import roomescape.theme.web.ThemeResponse; @@ -53,10 +53,10 @@ class ThemeServiceTest { @DisplayName("테마를 조회한다.") void findThemeById() { // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); // when - Theme foundTheme = themeService.findThemeById(theme.getId()); + ThemeEntity foundTheme = themeService.findThemeById(theme.getId()); // then assertThat(foundTheme).isEqualTo(theme); @@ -66,7 +66,7 @@ class ThemeServiceTest { @DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.") void findThemeByNotExistId() { // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); // when Long notExistId = theme.getId() + 1; @@ -80,8 +80,8 @@ class ThemeServiceTest { @DisplayName("모든 테마를 조회한다.") void findAllThemes() { // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1")); + ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); + ThemeEntity theme1 = themeRepository.save(new ThemeEntity("name1", "description1", "thumbnail1")); // when ThemesResponse found = themeService.findAllThemes(); @@ -111,7 +111,7 @@ class ThemeServiceTest { ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail")); // when - Theme found = themeRepository.findById(themeResponse.id()).orElse(null); + ThemeEntity found = themeRepository.findById(themeResponse.id()).orElse(null); // then assertThat(found).isNotNull(); @@ -135,7 +135,7 @@ class ThemeServiceTest { @DisplayName("테마를 삭제한다.") void removeThemeById() { // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); // when themeService.removeThemeById(theme.getId()); @@ -151,7 +151,7 @@ class ThemeServiceTest { LocalDateTime dateTime = LocalDateTime.now().plusDays(1); ReservationTimeResponse time = reservationTimeService.addTime( new ReservationTimeRequest(dateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); Member member = memberRepository.save(new Member(null, "member", "password", "name", Role.MEMBER)); reservationService.addReservation( new ReservationRequest(dateTime.toLocalDate(), time.id(), theme.getId(), "paymentKey", "orderId", 1000L, diff --git a/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt index daebd118..4111939a 100644 --- a/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt +++ b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt @@ -4,7 +4,7 @@ import jakarta.persistence.EntityManager import roomescape.member.infrastructure.persistence.Member import roomescape.reservation.domain.ReservationStatus import roomescape.reservation.domain.ReservationTime -import roomescape.theme.infrastructure.persistence.Theme +import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.util.MemberFixture import roomescape.util.ReservationFixture import roomescape.util.ReservationTimeFixture @@ -18,8 +18,8 @@ object TestThemeCreateUtil { name: String, reservedCount: Int, date: LocalDate, - ): Long { - val theme: Theme = ThemeFixture.create(name = name).also { entityManager.persist(it) } + ): ThemeEntity { + val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) } val member: Member = MemberFixture.create().also { entityManager.persist(it) } for (i in 1..reservedCount) { @@ -29,7 +29,7 @@ object TestThemeCreateUtil { ReservationFixture.create( date = date, - theme = theme, + themeEntity = themeEntity, member = member, reservationTime = time, status = ReservationStatus.CONFIRMED @@ -39,6 +39,6 @@ object TestThemeCreateUtil { entityManager.flush() entityManager.clear() - return theme.id + return themeEntity } } diff --git a/src/test/java/roomescape/util/Fixtures.kt b/src/test/java/roomescape/util/Fixtures.kt index bf9bd32d..5236ef28 100644 --- a/src/test/java/roomescape/util/Fixtures.kt +++ b/src/test/java/roomescape/util/Fixtures.kt @@ -11,7 +11,7 @@ import roomescape.payment.web.PaymentCancel import roomescape.reservation.domain.Reservation import roomescape.reservation.domain.ReservationStatus import roomescape.reservation.domain.ReservationTime -import roomescape.theme.infrastructure.persistence.Theme +import roomescape.theme.infrastructure.persistence.ThemeEntity import java.time.LocalDate import java.time.LocalTime import java.time.OffsetDateTime @@ -62,18 +62,18 @@ object ThemeFixture { name: String = "Default Theme", description: String = "Default Description", thumbnail: String = "https://example.com/default-thumbnail.jpg" - ): Theme = Theme(id, name, description, thumbnail) + ): ThemeEntity = ThemeEntity(id, name, description, thumbnail) } object ReservationFixture { fun create( id: Long? = null, date: LocalDate = LocalDate.now().plusWeeks(1), - theme: Theme = ThemeFixture.create(), + themeEntity: ThemeEntity = ThemeFixture.create(), reservationTime: ReservationTime = ReservationTimeFixture.create(), member: Member = MemberFixture.create(), status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - ): Reservation = Reservation(id, date, reservationTime, theme, member, status) + ): Reservation = Reservation(id, date, reservationTime, themeEntity, member, status) } object JwtFixture { -- 2.47.2 From 766f3b48a5f57ab08f97513735dd93f16a068061 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:16:05 +0900 Subject: [PATCH 11/17] Rename .java to .kt --- .../persistence/{ThemeRepository.java => ThemeRepository.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/roomescape/theme/infrastructure/persistence/{ThemeRepository.java => ThemeRepository.kt} (100%) diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt similarity index 100% rename from src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.java rename to src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt -- 2.47.2 From 90b86f4098f2694d82bd20048274582332bb4eca Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:16:05 +0900 Subject: [PATCH 12/17] =?UTF-8?q?refactor:=20ThemeEntity=20=EC=BD=94?= =?UTF-8?q?=ED=8B=80=EB=A6=B0=20=EC=A0=84=ED=99=98=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ThemeRepository.kt | 26 +-- .../persistence/ThemeRepositoryTest.kt | 155 ++++++++++++++++++ 2 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt index 0a48af56..f0ea0b5a 100644 --- a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt @@ -1,14 +1,12 @@ -package roomescape.theme.infrastructure.persistence; +package roomescape.theme.infrastructure.persistence -import java.time.LocalDate; -import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDate -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; +interface ThemeRepository : JpaRepository { -public interface ThemeRepository extends JpaRepository { - - @Query(value = """ + @Query(value = """ SELECT t FROM ThemeEntity t RIGHT JOIN Reservation r ON t.id = r.theme.id @@ -16,17 +14,19 @@ public interface ThemeRepository extends JpaRepository { GROUP BY r.theme.id ORDER BY COUNT(r.theme.id) DESC, t.id ASC LIMIT :limit - """) - List findTopNThemeBetweenStartDateAndEndDate(LocalDate startDate, LocalDate endDate, int limit); + + """ + ) + fun findTopNThemeBetweenStartDateAndEndDate(startDate: LocalDate, endDate: LocalDate, limit: Int): List - boolean existsByName(String name); + fun existsByName(name: String): Boolean - @Query(value = """ + @Query(value = """ SELECT EXISTS( SELECT 1 FROM Reservation r WHERE r.theme.id = :id ) """) - boolean isReservedTheme(Long id); + fun isReservedTheme(id: Long): Boolean } diff --git a/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt b/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt new file mode 100644 index 00000000..6780ecd0 --- /dev/null +++ b/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt @@ -0,0 +1,155 @@ +package roomescape.theme.infrastructure.persistence + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainInOrder +import io.kotest.matchers.shouldBe +import jakarta.persistence.EntityManager +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.transaction.support.TransactionTemplate +import roomescape.theme.util.TestThemeCreateUtil +import java.time.LocalDate + +@DataJpaTest +class ThemeRepositoryTest( + val themeRepository: ThemeRepository, + val transactionTemplate: TransactionTemplate, + val entityManager: EntityManager +) : FunSpec() { + + init { + beforeSpec { + /** + * 테마 10개를 생성한다. + * 이름: "테마N", 예약 수: N, 날짜: 오늘 기준으로 N일 전 + */ + transactionTemplate.executeWithoutResult { + for (i in 1..10) { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마$i", + reservedCount = i, + date = LocalDate.now().minusDays(i.toLong()), + ) + } + } + } + + context("findTopNThemeBetweenStartDateAndEndDate") { + test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(10), + LocalDate.now().minusDays(1), + 5 + ).also { themes -> + themes.size shouldBe 5 + themes.map { it.name } shouldContainInOrder listOf( + "테마10", "테마9", "테마8", "테마7", "테마6" + ) + } + } + + test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(8), + LocalDate.now().minusDays(5), + 3 + ).also { themes -> + themes.size shouldBe 3 + themes.map { it.name } shouldContainInOrder listOf( + "테마8", "테마7", "테마6" + ) + } + } + + test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마11", + reservedCount = 5, + date = LocalDate.now().minusDays(5), + ) + + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(6), + LocalDate.now().minusDays(4), + 5 + ).also { themes -> + themes.size shouldBe 4 + themes.map { it.name } shouldContainInOrder listOf( + "테마6", "테마5", "테마11", "테마4" + ) + } + } + + test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(10), + LocalDate.now().minusDays(6), + 10 + ).also { themes -> + themes.size shouldBe 5 + } + } + + test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(10), + LocalDate.now().minusDays(1), + 15 + ).also { themes -> + themes.size shouldBe 10 + } + } + + test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().plusDays(1), + LocalDate.now().plusDays(10), + 5 + ).also { themes -> + themes.size shouldBe 0 + } + } + } + context("existsByName ") { + val themeName = "test-theme" + beforeTest { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = themeName, + reservedCount = 0, + date = LocalDate.now() + ) + } + test("테마 이름이 존재하면 true를 반환한다.") { + themeRepository.existsByName(themeName) shouldBe true + } + + test("테마 이름이 존재하지 않으면 false를 반환한다.") { + themeRepository.existsByName(themeName.repeat(2)) shouldBe false + } + } + + context("isReservedTheme") { + test("테마가 예약 중이면 true를 반환한다.") { + val theme = TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "예약된 테마", + reservedCount = 1, + date = LocalDate.now() + ) + themeRepository.isReservedTheme(theme.id!!) shouldBe true + } + + test("테마가 예약 중이 아니면 false를 반환한다.") { + val theme = TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "예약되지 않은 테마", + reservedCount = 0, + date = LocalDate.now() + ) + themeRepository.isReservedTheme(theme.id!!) shouldBe false + } + } + } +} -- 2.47.2 From 60f5fd6b00aea86d2ce24dfe2cbe48ede6674013 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:17:03 +0900 Subject: [PATCH 13/17] Rename .java to .kt --- .../theme/business/{ThemeService.java => ThemeService.kt} | 0 .../theme/business/{ThemeServiceTest.java => ThemeServiceTest.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/main/java/roomescape/theme/business/{ThemeService.java => ThemeService.kt} (100%) rename src/test/java/roomescape/theme/business/{ThemeServiceTest.java => ThemeServiceTest.kt} (100%) diff --git a/src/main/java/roomescape/theme/business/ThemeService.java b/src/main/java/roomescape/theme/business/ThemeService.kt similarity index 100% rename from src/main/java/roomescape/theme/business/ThemeService.java rename to src/main/java/roomescape/theme/business/ThemeService.kt diff --git a/src/test/java/roomescape/theme/business/ThemeServiceTest.java b/src/test/java/roomescape/theme/business/ThemeServiceTest.kt similarity index 100% rename from src/test/java/roomescape/theme/business/ThemeServiceTest.java rename to src/test/java/roomescape/theme/business/ThemeServiceTest.kt -- 2.47.2 From 68de9179ad75700a1de77f9768eb263688fab8d5 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:17:03 +0900 Subject: [PATCH 14/17] =?UTF-8?q?refactor:=20ThemeService=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=ED=8B=80=EB=A6=B0=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/theme/business/ThemeService.kt | 133 +++++----- .../roomescape/theme/web/ThemeController.kt | 4 +- .../theme/business/ThemeServiceTest.kt | 231 +++++++----------- 3 files changed, 150 insertions(+), 218 deletions(-) diff --git a/src/main/java/roomescape/theme/business/ThemeService.kt b/src/main/java/roomescape/theme/business/ThemeService.kt index e900752d..1bb1199e 100644 --- a/src/main/java/roomescape/theme/business/ThemeService.kt +++ b/src/main/java/roomescape/theme/business/ThemeService.kt @@ -1,81 +1,74 @@ -package roomescape.theme.business; +package roomescape.theme.business -import java.time.LocalDate; -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import roomescape.common.exception.ErrorType; -import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.ThemeEntity; -import roomescape.theme.infrastructure.persistence.ThemeRepository; -import roomescape.theme.web.ThemeRequest; -import roomescape.theme.web.ThemeResponse; -import roomescape.theme.web.ThemesResponse; +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.theme.web.ThemeRequest +import roomescape.theme.web.ThemeResponse +import roomescape.theme.web.ThemesResponse +import roomescape.theme.web.toResponse +import java.time.LocalDate @Service -@Transactional -public class ThemeService { +class ThemeService( + private val themeRepository: ThemeRepository +) { + @Transactional(readOnly = true) + fun findThemeById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id) + ?: throw RoomescapeException( + ErrorType.THEME_NOT_FOUND, + "[themeId: $id]", + HttpStatus.BAD_REQUEST + ) - private final ThemeRepository themeRepository; + @Transactional(readOnly = true) + fun findAllThemes(): ThemesResponse = themeRepository.findAll() + .toResponse() - public ThemeService(ThemeRepository themeRepository) { - this.themeRepository = themeRepository; - } - @Transactional(readOnly = true) - public ThemeEntity findThemeById(Long id) { - return themeRepository.findById(id) - .orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND, - String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST)); - } + @Transactional(readOnly = true) + fun getMostReservedThemesByCount(count: Int): ThemesResponse { + val today = LocalDate.now() + val startDate = today.minusDays(7) + val endDate = today.minusDays(1) - @Transactional(readOnly = true) - public ThemesResponse findAllThemes() { - List response = themeRepository.findAll() - .stream() - .map(ThemeResponse::from) - .toList(); + return themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, count) + .toResponse() + } - return new ThemesResponse(response); - } + @Transactional + fun save(request: ThemeRequest): ThemeResponse { + if (themeRepository.existsByName(request.name)) { + throw RoomescapeException( + ErrorType.THEME_DUPLICATED, + "[name: ${request.name}]", + HttpStatus.CONFLICT + ) + } - @Transactional(readOnly = true) - public ThemesResponse getMostReservedThemesByCount(int count) { - LocalDate today = LocalDate.now(); - LocalDate startDate = today.minusDays(7); - LocalDate endDate = today.minusDays(1); + return ThemeEntity( + name = request.name, + description = request.description, + thumbnail = request.thumbnail + ).also { + themeRepository.save(it) + }.toResponse() + } - List response = themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, - count) - .stream() - .map(ThemeResponse::from) - .toList(); - - return new ThemesResponse(response); - } - - public ThemeResponse addTheme(ThemeRequest request) { - validateIsSameThemeNameExist(request.name()); - ThemeEntity theme = themeRepository.save(new ThemeEntity(request.name(), request.description(), request.thumbnail())); - - return ThemeResponse.from(theme); - } - - private void validateIsSameThemeNameExist(String name) { - if (themeRepository.existsByName(name)) { - throw new RoomescapeException(ErrorType.THEME_DUPLICATED, - String.format("[name: %s]", name), HttpStatus.CONFLICT); - } - } - - public void removeThemeById(Long id) { - if (themeRepository.isReservedTheme(id)) { - throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT, - String.format("[themeId: %d]", id), HttpStatus.CONFLICT); - } - themeRepository.deleteById(id); - } + @Transactional + fun deleteById(id: Long) { + if (themeRepository.isReservedTheme(id)) { + throw RoomescapeException( + ErrorType.THEME_IS_USED_CONFLICT, + "[themeId: %d]", + HttpStatus.CONFLICT + ) + } + themeRepository.deleteById(id) + } } diff --git a/src/main/java/roomescape/theme/web/ThemeController.kt b/src/main/java/roomescape/theme/web/ThemeController.kt index c7182b83..1a7a7ec1 100644 --- a/src/main/java/roomescape/theme/web/ThemeController.kt +++ b/src/main/java/roomescape/theme/web/ThemeController.kt @@ -34,7 +34,7 @@ class ThemeController( override fun saveTheme( @RequestBody @Valid request: ThemeRequest ): ResponseEntity> { - val themeResponse: ThemeResponse = themeService.addTheme(request) + val themeResponse: ThemeResponse = themeService.save(request) return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) .body(CommonApiResponse(themeResponse)) @@ -44,7 +44,7 @@ class ThemeController( override fun removeTheme( @PathVariable id: Long ): ResponseEntity> { - themeService.removeThemeById(id) + themeService.deleteById(id) return ResponseEntity.noContent().build() } diff --git a/src/test/java/roomescape/theme/business/ThemeServiceTest.kt b/src/test/java/roomescape/theme/business/ThemeServiceTest.kt index 3df47809..a244273c 100644 --- a/src/test/java/roomescape/theme/business/ThemeServiceTest.kt +++ b/src/test/java/roomescape/theme/business/ThemeServiceTest.kt @@ -1,164 +1,103 @@ -package roomescape.theme.business; +package roomescape.theme.business -import static org.assertj.core.api.Assertions.*; +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.theme.web.ThemeRequest +import roomescape.util.ThemeFixture -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; +class ThemeServiceTest : FunSpec({ -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.jdbc.Sql; + val themeRepository: ThemeRepository = mockk() + val themeService = ThemeService(themeRepository) -import roomescape.member.business.MemberService; -import roomescape.member.infrastructure.persistence.Member; -import roomescape.member.infrastructure.persistence.MemberRepository; -import roomescape.member.infrastructure.persistence.Role; -import roomescape.reservation.dto.request.ReservationRequest; -import roomescape.reservation.dto.request.ReservationTimeRequest; -import roomescape.reservation.dto.response.ReservationTimeResponse; -import roomescape.reservation.service.ReservationService; -import roomescape.reservation.service.ReservationTimeService; -import roomescape.common.exception.RoomescapeException; -import roomescape.theme.infrastructure.persistence.ThemeEntity; -import roomescape.theme.infrastructure.persistence.ThemeRepository; -import roomescape.theme.web.ThemeRequest; -import roomescape.theme.web.ThemeResponse; -import roomescape.theme.web.ThemesResponse; + context("findThemeById") { + val themeId = 1L + test("조회 성공") { + val theme: ThemeEntity = ThemeFixture.create(id = themeId) + every { + themeRepository.findByIdOrNull(themeId) + } returns theme -@DataJpaTest -@Import({ReservationTimeService.class, ReservationService.class, MemberService.class, ThemeService.class}) -@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -class ThemeServiceTest { + theme.id shouldBe themeId + } - @Autowired - private ThemeRepository themeRepository; + test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") { + every { + themeRepository.findByIdOrNull(themeId) + } returns null - @Autowired - private ThemeService themeService; + val exception = shouldThrow { + themeService.findThemeById(themeId) + } - @Autowired - private ReservationTimeService reservationTimeService; + exception.errorType shouldBe ErrorType.THEME_NOT_FOUND + } + } - @Autowired - private MemberRepository memberRepository; + context("findAllThemes") { + test("모든 테마를 조회한다.") { + val themes = listOf(ThemeFixture.create(id = 1, name = "t1"), ThemeFixture.create(id = 2, name = "t2")) + every { + themeRepository.findAll() + } returns themes - @Autowired - private ReservationService reservationService; + assertSoftly(themeService.findAllThemes()) { + this.themes.size shouldBe themes.size + this.themes[0].name shouldBe "t1" + this.themes[1].name shouldBe "t2" + } + } + } - @Test - @DisplayName("테마를 조회한다.") - void findThemeById() { - // given - ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); + context("save") { + test("테마 이름이 중복되면 409 예외를 던진다.") { + val name = "Duplicate Theme" - // when - ThemeEntity foundTheme = themeService.findThemeById(theme.getId()); + every { + themeRepository.existsByName(name) + } returns true - // then - assertThat(foundTheme).isEqualTo(theme); - } + val exception = shouldThrow { + themeService.save(ThemeRequest( + name = name, + description = "Description", + thumbnail = "http://example.com/thumbnail.jpg" + )) + } - @Test - @DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.") - void findThemeByNotExistId() { - // given - ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); + assertSoftly(exception) { + this.errorType shouldBe ErrorType.THEME_DUPLICATED + this.httpStatus shouldBe HttpStatus.CONFLICT + } + } + } - // when - Long notExistId = theme.getId() + 1; + context("deleteById") { + test("이미 예약 중인 테마라면 409 예외를 던진다.") { + val themeId = 1L - // then - assertThatThrownBy(() -> themeService.findThemeById(notExistId)) - .isInstanceOf(RoomescapeException.class); - } + every { + themeRepository.isReservedTheme(themeId) + } returns true - @Test - @DisplayName("모든 테마를 조회한다.") - void findAllThemes() { - // given - ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); - ThemeEntity theme1 = themeRepository.save(new ThemeEntity("name1", "description1", "thumbnail1")); + val exception = shouldThrow { + themeService.deleteById(themeId) + } - // when - ThemesResponse found = themeService.findAllThemes(); - - // then - assertThat(found.themes()).extracting("id").containsExactly(theme.getId(), theme1.getId()); - } - - @Test - @DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.") - @Sql({"/truncate.sql", "/reservationData.sql"}) - void getMostReservedThemesByCount() { - // given - LocalDate today = LocalDate.now(); - - // when - List found = themeService.getMostReservedThemesByCount(10).themes(); - - // then : 11번 테마는 조회되지 않아야 한다. - assertThat(found).extracting("id").containsExactly(1L, 4L, 2L, 6L, 3L, 5L, 7L, 8L, 9L, 10L); - } - - @Test - @DisplayName("테마를 추가한다.") - void addTheme() { - // given - ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail")); - - // when - ThemeEntity found = themeRepository.findById(themeResponse.id()).orElse(null); - - // then - assertThat(found).isNotNull(); - } - - @Test - @DisplayName("테마를 추가할 때 같은 이름의 테마가 존재하면 예외가 발생한다. ") - void addDuplicateTheme() { - // given - ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail")); - - // when - ThemeRequest invalidRequest = new ThemeRequest(themeResponse.name(), "description", "thumbnail"); - - // then - assertThatThrownBy(() -> themeService.addTheme(invalidRequest)) - .isInstanceOf(RoomescapeException.class); - } - - @Test - @DisplayName("테마를 삭제한다.") - void removeThemeById() { - // given - ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); - - // when - themeService.removeThemeById(theme.getId()); - - // then - assertThat(themeRepository.findById(theme.getId())).isEmpty(); - } - - @Test - @DisplayName("예약이 존재하는 테마를 삭제하면 예외가 발생한다.") - void removeReservedTheme() { - // given - LocalDateTime dateTime = LocalDateTime.now().plusDays(1); - ReservationTimeResponse time = reservationTimeService.addTime( - new ReservationTimeRequest(dateTime.toLocalTime())); - ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail")); - Member member = memberRepository.save(new Member(null, "member", "password", "name", Role.MEMBER)); - reservationService.addReservation( - new ReservationRequest(dateTime.toLocalDate(), time.id(), theme.getId(), "paymentKey", "orderId", 1000L, - "NORMAL"), member.getId()); - - // when & then - assertThatThrownBy(() -> themeService.removeThemeById(theme.getId())) - .isInstanceOf(RoomescapeException.class); - } -} + assertSoftly(exception) { + this.errorType shouldBe ErrorType.THEME_IS_USED_CONFLICT + this.httpStatus shouldBe HttpStatus.CONFLICT + } + } + } +}) -- 2.47.2 From bc8ab293550c698bf4c9b3dd0edc77eb8b86c9b1 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:29:34 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dto/response/RoomescapeErrorResponse.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt b/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt index 045a33c2..819eb297 100644 --- a/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt +++ b/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt @@ -8,11 +8,4 @@ import roomescape.common.exception.ErrorType data class RoomescapeErrorResponse( val errorType: ErrorType, val message: String -) { - - companion object { - @JvmStatic - fun of(errorType: ErrorType, message: String? = null): RoomescapeErrorResponse = - RoomescapeErrorResponse(errorType, message ?: errorType.description) - } -} +) -- 2.47.2 From f517752e4469c7e22dfe28e8b9a855a457dd63d6 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:30:25 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20\@SpringBootTest=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=ED=9B=84=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=A1=A4?= =?UTF-8?q?=EB=B0=B1=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/theme/web/MostReservedThemeAPITest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt index f050cfaf..8931f6a2 100644 --- a/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt +++ b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt @@ -8,6 +8,7 @@ import jakarta.persistence.EntityManager import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.context.jdbc.Sql import org.springframework.transaction.support.TransactionTemplate import roomescape.theme.business.ThemeService import roomescape.theme.util.TestThemeCreateUtil @@ -20,13 +21,13 @@ import kotlin.random.Random * 날짜 범위, 예약 수만 검증 */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(value = ["/truncate.sql"], executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS) class MostReservedThemeAPITest( @LocalServerPort val port: Int, val themeService: ThemeService, val transactionTemplate: TransactionTemplate, val entityManager: EntityManager, ) : FunSpec() { - init { beforeSpec { transactionTemplate.executeWithoutResult { @@ -50,7 +51,7 @@ class MostReservedThemeAPITest( } } - context("가장 많이 예약된 테마를 조회할 때,") { + context("가장 많이 예약된 테마를 조회할 때, ") { val endpoint = "/themes/most-reserved-last-week" test("갯수를 입력하지 않으면 10개를 반환한다.") { Given { -- 2.47.2 From 6165d173cea2fb9189b69fb0c295aa9ea2bba258 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:31:00 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EA=B0=84=20=EA=B2=A9=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20beforeSpec=20->=20beforeTest=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ThemeRepositoryTest.kt | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt b/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt index 6780ecd0..73df4b11 100644 --- a/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt +++ b/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt @@ -5,24 +5,18 @@ import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.shouldBe import jakarta.persistence.EntityManager import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.transaction.support.TransactionTemplate import roomescape.theme.util.TestThemeCreateUtil import java.time.LocalDate @DataJpaTest class ThemeRepositoryTest( val themeRepository: ThemeRepository, - val transactionTemplate: TransactionTemplate, val entityManager: EntityManager ) : FunSpec() { init { - beforeSpec { - /** - * 테마 10개를 생성한다. - * 이름: "테마N", 예약 수: N, 날짜: 오늘 기준으로 N일 전 - */ - transactionTemplate.executeWithoutResult { + context("findTopNThemeBetweenStartDateAndEndDate") { + beforeTest { for (i in 1..10) { TestThemeCreateUtil.createThemeWithReservations( entityManager = entityManager, @@ -32,9 +26,7 @@ class ThemeRepositoryTest( ) } } - } - context("findTopNThemeBetweenStartDateAndEndDate") { test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") { themeRepository.findTopNThemeBetweenStartDateAndEndDate( LocalDate.now().minusDays(10), @@ -115,10 +107,10 @@ class ThemeRepositoryTest( val themeName = "test-theme" beforeTest { TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, - name = themeName, - reservedCount = 0, - date = LocalDate.now() + entityManager = entityManager, + name = themeName, + reservedCount = 0, + date = LocalDate.now() ) } test("테마 이름이 존재하면 true를 반환한다.") { @@ -133,20 +125,20 @@ class ThemeRepositoryTest( context("isReservedTheme") { test("테마가 예약 중이면 true를 반환한다.") { val theme = TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, - name = "예약된 테마", - reservedCount = 1, - date = LocalDate.now() + entityManager = entityManager, + name = "예약된 테마", + reservedCount = 1, + date = LocalDate.now() ) themeRepository.isReservedTheme(theme.id!!) shouldBe true } test("테마가 예약 중이 아니면 false를 반환한다.") { val theme = TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, - name = "예약되지 않은 테마", - reservedCount = 0, - date = LocalDate.now() + entityManager = entityManager, + name = "예약되지 않은 테마", + reservedCount = 0, + date = LocalDate.now() ) themeRepository.isReservedTheme(theme.id!!) shouldBe false } -- 2.47.2