refactor: ThemeService 및 테스트 코틀린 전환

This commit is contained in:
이상진 2025-07-18 01:17:03 +09:00
parent 60f5fd6b00
commit 68de9179ad
3 changed files with 150 additions and 218 deletions

View File

@ -1,81 +1,74 @@
package roomescape.theme.business; package roomescape.theme.business
import java.time.LocalDate; import org.springframework.data.repository.findByIdOrNull
import java.util.List; import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.http.HttpStatus; import org.springframework.transaction.annotation.Transactional
import org.springframework.stereotype.Service; import roomescape.common.exception.ErrorType
import org.springframework.transaction.annotation.Transactional; import roomescape.common.exception.RoomescapeException
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.common.exception.ErrorType; import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.common.exception.RoomescapeException; import roomescape.theme.web.ThemeRequest
import roomescape.theme.infrastructure.persistence.ThemeEntity; import roomescape.theme.web.ThemeResponse
import roomescape.theme.infrastructure.persistence.ThemeRepository; import roomescape.theme.web.ThemesResponse
import roomescape.theme.web.ThemeRequest; import roomescape.theme.web.toResponse
import roomescape.theme.web.ThemeResponse; import java.time.LocalDate
import roomescape.theme.web.ThemesResponse;
@Service @Service
@Transactional class ThemeService(
public class ThemeService { private val themeRepository: ThemeRepository
) {
private final ThemeRepository themeRepository; @Transactional(readOnly = true)
fun findThemeById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
public ThemeService(ThemeRepository themeRepository) { ?: throw RoomescapeException(
this.themeRepository = themeRepository; ErrorType.THEME_NOT_FOUND,
} "[themeId: $id]",
HttpStatus.BAD_REQUEST
)
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ThemeEntity findThemeById(Long id) { fun findAllThemes(): ThemesResponse = themeRepository.findAll()
return themeRepository.findById(id) .toResponse()
.orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND,
String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST));
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ThemesResponse findAllThemes() { fun getMostReservedThemesByCount(count: Int): ThemesResponse {
List<ThemeResponse> response = themeRepository.findAll() val today = LocalDate.now()
.stream() val startDate = today.minusDays(7)
.map(ThemeResponse::from) val endDate = today.minusDays(1)
.toList();
return new ThemesResponse(response); return themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, count)
.toResponse()
} }
@Transactional(readOnly = true) @Transactional
public ThemesResponse getMostReservedThemesByCount(int count) { fun save(request: ThemeRequest): ThemeResponse {
LocalDate today = LocalDate.now(); if (themeRepository.existsByName(request.name)) {
LocalDate startDate = today.minusDays(7); throw RoomescapeException(
LocalDate endDate = today.minusDays(1); ErrorType.THEME_DUPLICATED,
"[name: ${request.name}]",
List<ThemeResponse> response = themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, HttpStatus.CONFLICT
count) )
.stream()
.map(ThemeResponse::from)
.toList();
return new ThemesResponse(response);
} }
public ThemeResponse addTheme(ThemeRequest request) { return ThemeEntity(
validateIsSameThemeNameExist(request.name()); name = request.name,
ThemeEntity theme = themeRepository.save(new ThemeEntity(request.name(), request.description(), request.thumbnail())); description = request.description,
thumbnail = request.thumbnail
return ThemeResponse.from(theme); ).also {
themeRepository.save(it)
}.toResponse()
} }
private void validateIsSameThemeNameExist(String name) { @Transactional
if (themeRepository.existsByName(name)) { fun deleteById(id: Long) {
throw new RoomescapeException(ErrorType.THEME_DUPLICATED,
String.format("[name: %s]", name), HttpStatus.CONFLICT);
}
}
public void removeThemeById(Long id) {
if (themeRepository.isReservedTheme(id)) { if (themeRepository.isReservedTheme(id)) {
throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT, throw RoomescapeException(
String.format("[themeId: %d]", id), HttpStatus.CONFLICT); ErrorType.THEME_IS_USED_CONFLICT,
"[themeId: %d]",
HttpStatus.CONFLICT
)
} }
themeRepository.deleteById(id); themeRepository.deleteById(id)
} }
} }

View File

@ -34,7 +34,7 @@ class ThemeController(
override fun saveTheme( override fun saveTheme(
@RequestBody @Valid request: ThemeRequest @RequestBody @Valid request: ThemeRequest
): ResponseEntity<CommonApiResponse<ThemeResponse>> { ): ResponseEntity<CommonApiResponse<ThemeResponse>> {
val themeResponse: ThemeResponse = themeService.addTheme(request) val themeResponse: ThemeResponse = themeService.save(request)
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
.body(CommonApiResponse(themeResponse)) .body(CommonApiResponse(themeResponse))
@ -44,7 +44,7 @@ class ThemeController(
override fun removeTheme( override fun removeTheme(
@PathVariable id: Long @PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
themeService.removeThemeById(id) themeService.deleteById(id)
return ResponseEntity.noContent().build() return ResponseEntity.noContent().build()
} }

View File

@ -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; class ThemeServiceTest : FunSpec({
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.DisplayName; val themeRepository: ThemeRepository = mockk()
import org.junit.jupiter.api.Test; val themeService = ThemeService(themeRepository)
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;
import roomescape.member.business.MemberService; context("findThemeById") {
import roomescape.member.infrastructure.persistence.Member; val themeId = 1L
import roomescape.member.infrastructure.persistence.MemberRepository; test("조회 성공") {
import roomescape.member.infrastructure.persistence.Role; val theme: ThemeEntity = ThemeFixture.create(id = themeId)
import roomescape.reservation.dto.request.ReservationRequest; every {
import roomescape.reservation.dto.request.ReservationTimeRequest; themeRepository.findByIdOrNull(themeId)
import roomescape.reservation.dto.response.ReservationTimeResponse; } returns theme
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;
@DataJpaTest theme.id shouldBe themeId
@Import({ReservationTimeService.class, ReservationService.class, MemberService.class, ThemeService.class})
@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
class ThemeServiceTest {
@Autowired
private ThemeRepository themeRepository;
@Autowired
private ThemeService themeService;
@Autowired
private ReservationTimeService reservationTimeService;
@Autowired
private MemberRepository memberRepository;
@Autowired
private ReservationService reservationService;
@Test
@DisplayName("테마를 조회한다.")
void findThemeById() {
// given
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
// when
ThemeEntity foundTheme = themeService.findThemeById(theme.getId());
// then
assertThat(foundTheme).isEqualTo(theme);
} }
@Test test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") {
@DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.") every {
void findThemeByNotExistId() { themeRepository.findByIdOrNull(themeId)
// given } returns null
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
// when val exception = shouldThrow<RoomescapeException> {
Long notExistId = theme.getId() + 1; themeService.findThemeById(themeId)
// then
assertThatThrownBy(() -> themeService.findThemeById(notExistId))
.isInstanceOf(RoomescapeException.class);
} }
@Test exception.errorType shouldBe ErrorType.THEME_NOT_FOUND
@DisplayName("모든 테마를 조회한다.") }
void findAllThemes() {
// given
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
ThemeEntity theme1 = themeRepository.save(new ThemeEntity("name1", "description1", "thumbnail1"));
// when
ThemesResponse found = themeService.findAllThemes();
// then
assertThat(found.themes()).extracting("id").containsExactly(theme.getId(), theme1.getId());
} }
@Test context("findAllThemes") {
@DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.") test("모든 테마를 조회한다.") {
@Sql({"/truncate.sql", "/reservationData.sql"}) val themes = listOf(ThemeFixture.create(id = 1, name = "t1"), ThemeFixture.create(id = 2, name = "t2"))
void getMostReservedThemesByCount() { every {
// given themeRepository.findAll()
LocalDate today = LocalDate.now(); } returns themes
// when assertSoftly(themeService.findAllThemes()) {
List<ThemeResponse> found = themeService.getMostReservedThemesByCount(10).themes(); this.themes.size shouldBe themes.size
this.themes[0].name shouldBe "t1"
// then : 11번 테마는 조회되지 않아야 한다. this.themes[1].name shouldBe "t2"
assertThat(found).extracting("id").containsExactly(1L, 4L, 2L, 6L, 3L, 5L, 7L, 8L, 9L, 10L); }
}
} }
@Test context("save") {
@DisplayName("테마를 추가한다.") test("테마 이름이 중복되면 409 예외를 던진다.") {
void addTheme() { val name = "Duplicate Theme"
// given
ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail"));
// when every {
ThemeEntity found = themeRepository.findById(themeResponse.id()).orElse(null); themeRepository.existsByName(name)
} returns true
// then val exception = shouldThrow<RoomescapeException> {
assertThat(found).isNotNull(); themeService.save(ThemeRequest(
name = name,
description = "Description",
thumbnail = "http://example.com/thumbnail.jpg"
))
} }
@Test assertSoftly(exception) {
@DisplayName("테마를 추가할 때 같은 이름의 테마가 존재하면 예외가 발생한다. ") this.errorType shouldBe ErrorType.THEME_DUPLICATED
void addDuplicateTheme() { this.httpStatus shouldBe HttpStatus.CONFLICT
// 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 context("deleteById") {
@DisplayName("테마를 삭제한다.") test("이미 예약 중인 테마라면 409 예외를 던진다.") {
void removeThemeById() { val themeId = 1L
// given
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
// when every {
themeService.removeThemeById(theme.getId()); themeRepository.isReservedTheme(themeId)
} returns true
// then val exception = shouldThrow<RoomescapeException> {
assertThat(themeRepository.findById(theme.getId())).isEmpty(); themeService.deleteById(themeId)
} }
@Test assertSoftly(exception) {
@DisplayName("예약이 존재하는 테마를 삭제하면 예외가 발생한다.") this.errorType shouldBe ErrorType.THEME_IS_USED_CONFLICT
void removeReservedTheme() { this.httpStatus shouldBe HttpStatus.CONFLICT
// 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);
} }
} }
}
})