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() {
this.spec = Specification.where(null);
}
fun sameDate(date: LocalDate?): ReservationSearchSpecification = andIfNotNull(date?.let {
Specification { root, _, cb ->
cb.equal(root.get<LocalDate>("date"), date)
}
})
public ReservationSearchSpecification sameThemeId(Long themeId) {
if (themeId != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("theme").get("id"), themeId));
}
return this;
}
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 sameMemberId(Long memberId) {
if (memberId != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("member").get("id"), memberId));
}
return this;
}
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
cb.equal(
root.get<ReservationStatus>("reservationStatus"),
ReservationStatus.WAITING
)
}
public ReservationSearchSpecification sameTimeId(Long timeId) {
if (timeId != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("reservationTime").get("id"),
timeId));
}
return this;
}
fun dateStartFrom(dateFrom: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateFrom?.let {
Specification { root, _, cb ->
cb.greaterThanOrEqualTo(root.get("date"), dateFrom)
}
})
public ReservationSearchSpecification sameDate(LocalDate date) {
if (date != null) {
this.spec = this.spec.and(
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("date"), date));
}
return this;
}
fun dateEndAt(dateTo: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateTo?.let {
Specification { root, _, cb ->
cb.lessThanOrEqualTo(root.get("date"), dateTo)
}
})
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;
}
fun build(): Specification<Reservation> {
return this.spec
}
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;
}
private fun andIfNotNull(condition: Specification<Reservation>?): ReservationSearchSpecification {
condition?.let { this.spec = this.spec.and(condition) }
return this
}
}

View File

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

View File

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

View File

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