From 68de9179ad75700a1de77f9768eb263688fab8d5 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 01:17:03 +0900 Subject: [PATCH] =?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 + } + } + } +})