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

This commit is contained in:
이상진 2025-07-18 06:02:59 +09:00
parent b20220794c
commit 5a919eb3ab
4 changed files with 211 additions and 214 deletions

View File

@ -1,84 +1,75 @@
package roomescape.reservation.infrastructure.persistence; package roomescape.reservation.infrastructure.persistence
import java.time.LocalDate; import org.springframework.data.jpa.domain.Specification
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.theme.infrastructure.persistence.ThemeEntity
import java.time.LocalDate
import org.springframework.data.jpa.domain.Specification; class ReservationSearchSpecification(
private var spec: Specification<Reservation> = Specification { _, _, _ -> null }
) {
fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let {
Specification { root, _, cb ->
cb.equal(root.get<ThemeEntity>("theme").get<Long>("id"), themeId)
}
})
public class ReservationSearchSpecification { fun sameMemberId(memberId: Long?): ReservationSearchSpecification = andIfNotNull(memberId?.let {
Specification { root, _, cb ->
cb.equal(root.get<MemberEntity>("member").get<Long>("id"), memberId)
}
})
private Specification<Reservation> spec; fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let {
Specification { root, _, cb ->
cb.equal(root.get<ReservationTime>("reservationTime").get<Long>("id"), timeId)
}
})
public ReservationSearchSpecification() { fun sameDate(date: LocalDate?): ReservationSearchSpecification = andIfNotNull(date?.let {
this.spec = Specification.where(null); Specification { root, _, cb ->
cb.equal(root.get<LocalDate>("date"), date)
}
})
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
cb.or(
cb.equal(
root.get<ReservationStatus>("reservationStatus"),
ReservationStatus.CONFIRMED
),
cb.equal(
root.get<ReservationStatus>("reservationStatus"),
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
)
)
} }
public ReservationSearchSpecification sameThemeId(Long themeId) { fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
if (themeId != null) { cb.equal(
this.spec = this.spec.and( root.get<ReservationStatus>("reservationStatus"),
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("theme").get("id"), themeId)); ReservationStatus.WAITING
} )
return this;
} }
public ReservationSearchSpecification sameMemberId(Long memberId) { fun dateStartFrom(dateFrom: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateFrom?.let {
if (memberId != null) { Specification { root, _, cb ->
this.spec = this.spec.and( cb.greaterThanOrEqualTo(root.get("date"), dateFrom)
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("member").get("id"), memberId));
} }
return this; })
fun dateEndAt(dateTo: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateTo?.let {
Specification { root, _, cb ->
cb.lessThanOrEqualTo(root.get("date"), dateTo)
}
})
fun build(): Specification<Reservation> {
return this.spec
} }
public ReservationSearchSpecification sameTimeId(Long timeId) { private fun andIfNotNull(condition: Specification<Reservation>?): ReservationSearchSpecification {
if (timeId != null) { condition?.let { this.spec = this.spec.and(condition) }
this.spec = this.spec.and( return this
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("reservationTime").get("id"),
timeId));
}
return this;
}
public ReservationSearchSpecification sameDate(LocalDate date) {
if (date != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("date"), date));
}
return this;
}
public ReservationSearchSpecification confirmed() {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.or(
criteriaBuilder.equal(root.get("reservationStatus"), ReservationStatus.CONFIRMED),
criteriaBuilder.equal(root.get("reservationStatus"),
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
));
return this;
}
public ReservationSearchSpecification waiting() {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("reservationStatus"),
ReservationStatus.WAITING));
return this;
}
public ReservationSearchSpecification dateStartFrom(LocalDate dateFrom) {
if (dateFrom != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get("date"), dateFrom));
}
return this;
}
public ReservationSearchSpecification dateEndAt(LocalDate toDate) {
if (toDate != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get("date"), toDate));
}
return this;
}
public Specification<Reservation> build() {
return this.spec;
} }
} }

View File

@ -1,173 +1,179 @@
package roomescape.reservation.infrastructure.persistence; package roomescape.reservation.infrastructure.persistence
import static org.assertj.core.api.Assertions.*; import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.StringSpec
import java.time.LocalDate; import io.kotest.matchers.collections.shouldContainExactly
import java.time.LocalDateTime; import io.kotest.matchers.collections.shouldHaveSize
import java.util.List; import jakarta.persistence.EntityManager
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.junit.jupiter.api.BeforeEach; import roomescape.member.infrastructure.persistence.MemberEntity
import org.junit.jupiter.api.DisplayName; import roomescape.theme.infrastructure.persistence.ThemeEntity
import org.junit.jupiter.api.Test; import roomescape.util.MemberFixture
import org.springframework.beans.factory.annotation.Autowired; import roomescape.util.ReservationFixture
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import roomescape.util.ReservationTimeFixture
import org.springframework.data.jpa.domain.Specification; import roomescape.util.ThemeFixture
import java.time.LocalDate
import roomescape.member.infrastructure.persistence.MemberEntity;
import roomescape.member.infrastructure.persistence.MemberRepository;
import roomescape.member.infrastructure.persistence.Role;
import roomescape.theme.infrastructure.persistence.ThemeEntity;
import roomescape.theme.infrastructure.persistence.ThemeRepository;
@DataJpaTest @DataJpaTest
class ReservationSearchSpecificationTest { class ReservationSearchSpecificationTest(
val entityManager: EntityManager,
val reservationRepository: ReservationRepository
) : StringSpec() {
@Autowired init {
private ReservationRepository reservationRepository; lateinit var confirmedNow: Reservation
lateinit var confirmedNotPaidYesterday: Reservation
lateinit var waitingTomorrow: Reservation
lateinit var member: MemberEntity
lateinit var reservationTime: ReservationTime
lateinit var theme: ThemeEntity
@Autowired "동일한 테마의 예약을 조회한다" {
private ReservationTimeRepository timeRepository; val spec = ReservationSearchSpecification()
.sameThemeId(theme.id)
.build()
@Autowired val results: List<Reservation> = reservationRepository.findAll(spec)
private ThemeRepository themeRepository;
@Autowired assertSoftly(results) {
private MemberRepository memberRepository; this shouldHaveSize 3
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
/** }
* 시간은 모두 현재 시간(LocalTime.now()), 테마, 회원은 동일 확정된 예약은 오늘, 결제 대기인 예약은 어제, 대기 상태인 예약은 내일
*/
// 현재 시간으로 확정 예약
private Reservation reservation1;
// 확정되었으나 결제 대기인 하루 전 예약
private Reservation reservation2;
// 대기 상태인 내일 예약
private Reservation reservation3;
@BeforeEach
void setUp() {
LocalDateTime dateTime = LocalDateTime.now();
MemberEntity member = memberRepository.save(
new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER));
ReservationTime time = timeRepository.save(new ReservationTime(dateTime.toLocalTime()));
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "description", "thumbnail"));
reservation1 = reservationRepository.save(
new Reservation(dateTime.toLocalDate(), time, theme, member, ReservationStatus.CONFIRMED));
reservation2 = reservationRepository.save(
new Reservation(dateTime.toLocalDate().minusDays(1), time, theme, member,
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
reservation3 = reservationRepository.save(
new Reservation(dateTime.toLocalDate().plusDays(1), time, theme, member, ReservationStatus.WAITING));
} }
@Test "동일한 회원의 예약을 조회한다" {
@DisplayName("동일한 테마의 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchByThemeId() { .sameMemberId(member.id)
// given .build()
Long themeId = reservation1.getTheme().getId();
Specification<Reservation> spec = new ReservationSearchSpecification().sameThemeId(themeId).build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation1, reservation2, reservation3); this shouldHaveSize 3
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
}
} }
@Test "동일한 예약 시간의 예약을 조회한다" {
@DisplayName("동일한 회원의 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchByMemberId() { .sameTimeId(reservationTime.id)
// given .build()
Long memberId = reservation1.getMember().getId();
Specification<Reservation> spec = new ReservationSearchSpecification().sameMemberId(memberId).build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation1, reservation2, reservation3); this shouldHaveSize 3
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
}
} }
@Test "동일한 날짜의 예약을 조회한다" {
@DisplayName("동일한 시간의 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchByTimeId() { .sameDate(LocalDate.now())
// given .build()
Long timeId = reservation1.getReservationTime().getId();
Specification<Reservation> spec = new ReservationSearchSpecification().sameTimeId(timeId).build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation1, reservation2, reservation3); this shouldHaveSize 1
this shouldContainExactly listOf(confirmedNow)
}
} }
@Test "확정 상태인 예약을 조회한다" {
@DisplayName("동일한 날짜의 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchByDate() { .confirmed()
// given .build()
LocalDate date = reservation1.getDate();
Specification<Reservation> spec = new ReservationSearchSpecification().sameDate(date).build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation1); this shouldHaveSize 2
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday)
}
} }
@Test "대기 상태인 예약을 조회한다" {
@DisplayName("확정 상태인 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchConfirmedReservation() { .waiting()
// given .build()
Specification<Reservation> spec = new ReservationSearchSpecification().confirmed().build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation1, reservation2); this shouldHaveSize 1
this shouldContainExactly listOf(waitingTomorrow)
}
} }
@Test "예약 날짜가 오늘 이후인 예약을 조회한다" {
@DisplayName("대기 중인 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchWaitingReservation() { .dateStartFrom(LocalDate.now())
// given .build()
Specification<Reservation> spec = new ReservationSearchSpecification().waiting().build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation3); this shouldHaveSize 2
this shouldContainExactly listOf(confirmedNow, waitingTomorrow)
}
} }
@Test "예약 날짜가 내일 이전인 예약을 조회한다" {
@DisplayName("특정 날짜 이후의 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchDateStartFrom() { .dateEndAt(LocalDate.now().plusDays(1))
// given : 어제 이후의 예약을 조회하면, 모든 예약이 조회되어야 한다. .build()
LocalDate date = LocalDate.now().minusDays(1L);
Specification<Reservation> spec = new ReservationSearchSpecification().dateStartFrom(date).build();
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
// then assertSoftly(results) {
assertThat(found).containsExactly(reservation1, reservation2, reservation3); this shouldHaveSize 3
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
}
} }
@Test beforeTest {
@DisplayName("특정 날짜 이전의 예약을 찾는다.") member = MemberFixture.create().also {
void searchDateEndAt() { entityManager.persist(it)
// given : 내일 이전의 예약을 조회하면, 모든 예약이 조회되어야 한다. }
LocalDate date = LocalDate.now().plusDays(1L); reservationTime = ReservationTimeFixture.create().also {
Specification<Reservation> spec = new ReservationSearchSpecification().dateEndAt(date).build(); entityManager.persist(it)
}
theme = ThemeFixture.create().also {
entityManager.persist(it)
}
// when confirmedNow = ReservationFixture.create(
List<Reservation> found = reservationRepository.findAll(spec); reservationTime = reservationTime,
member = member,
theme = theme,
date = LocalDate.now(),
status = ReservationStatus.CONFIRMED
).also {
entityManager.persist(it)
}
// then confirmedNotPaidYesterday = ReservationFixture.create(
assertThat(found).containsExactly(reservation1, reservation2, reservation3); reservationTime = reservationTime,
member = member,
theme = theme,
date = LocalDate.now().minusDays(1),
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
).also {
entityManager.persist(it)
}
waitingTomorrow = ReservationFixture.create(
reservationTime = reservationTime,
member = member,
theme = theme,
date = LocalDate.now().plusDays(1),
status = ReservationStatus.WAITING
).also {
entityManager.persist(it)
}
entityManager.flush()
}
} }
} }

View File

@ -29,7 +29,7 @@ object TestThemeCreateUtil {
ReservationFixture.create( ReservationFixture.create(
date = date, date = date,
themeEntity = themeEntity, theme = themeEntity,
member = member, member = member,
reservationTime = time, reservationTime = time,
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED

View File

@ -71,11 +71,11 @@ object ReservationFixture {
fun create( fun create(
id: Long? = null, id: Long? = null,
date: LocalDate = LocalDate.now().plusWeeks(1), date: LocalDate = LocalDate.now().plusWeeks(1),
themeEntity: ThemeEntity = ThemeFixture.create(), theme: ThemeEntity = ThemeFixture.create(),
reservationTime: ReservationTime = ReservationTimeFixture.create(), reservationTime: ReservationTime = ReservationTimeFixture.create(),
member: MemberEntity = MemberFixture.create(), member: MemberEntity = MemberFixture.create(),
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
): Reservation = Reservation(id, date, reservationTime, themeEntity, member, status) ): Reservation = Reservation(id, date, reservationTime, theme, member, status)
} }
object JwtFixture { object JwtFixture {