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 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<ThemeResponse> 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<ThemeResponse> 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)
}
}

View File

@ -34,7 +34,7 @@ class ThemeController(
override fun saveTheme(
@RequestBody @Valid request: ThemeRequest
): ResponseEntity<CommonApiResponse<ThemeResponse>> {
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<CommonApiResponse<Unit>> {
themeService.removeThemeById(id)
themeService.deleteById(id)
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;
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<RoomescapeException> {
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<RoomescapeException> {
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<RoomescapeException> {
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<ThemeResponse> 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
}
}
}
})