[#16] Reservation 도메인 코드 코틀린 마이그레이션 #17

Merged
pricelees merged 40 commits from refactor/#16 into main 2025-07-21 12:08:56 +00:00
4 changed files with 211 additions and 214 deletions
Showing only changes of commit 5a919eb3ab - Show all commits

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; val spec = ReservationSearchSpecification()
// 확정되었으나 결제 대기인 하루 전 예약 .sameMemberId(member.id)
private Reservation reservation2; .build()
// 대기 상태인 내일 예약
private Reservation reservation3; val results: List<Reservation> = reservationRepository.findAll(spec)
@BeforeEach assertSoftly(results) {
void setUp() { this shouldHaveSize 3
LocalDateTime dateTime = LocalDateTime.now(); this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
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 spec = ReservationSearchSpecification()
reservation1 = reservationRepository.save( .sameTimeId(reservationTime.id)
new Reservation(dateTime.toLocalDate(), time, theme, member, ReservationStatus.CONFIRMED)); .build()
reservation2 = reservationRepository.save(
new Reservation(dateTime.toLocalDate().minusDays(1), time, theme, member, val results: List<Reservation> = reservationRepository.findAll(spec)
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
reservation3 = reservationRepository.save( assertSoftly(results) {
new Reservation(dateTime.toLocalDate().plusDays(1), time, theme, member, ReservationStatus.WAITING)); this shouldHaveSize 3
} this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
}
@Test }
@DisplayName("동일한 테마의 예약을 찾는다.")
void searchByThemeId() { "동일한 날짜의 예약을 조회한다" {
// given val spec = ReservationSearchSpecification()
Long themeId = reservation1.getTheme().getId(); .sameDate(LocalDate.now())
Specification<Reservation> spec = new ReservationSearchSpecification().sameThemeId(themeId).build(); .build()
// when val results: List<Reservation> = reservationRepository.findAll(spec)
List<Reservation> found = reservationRepository.findAll(spec);
assertSoftly(results) {
// then this shouldHaveSize 1
assertThat(found).containsExactly(reservation1, reservation2, reservation3); this shouldContainExactly listOf(confirmedNow)
} }
}
@Test
@DisplayName("동일한 회원의 예약을 찾는다.") "확정 상태인 예약을 조회한다" {
void searchByMemberId() { val spec = ReservationSearchSpecification()
// given .confirmed()
Long memberId = reservation1.getMember().getId(); .build()
Specification<Reservation> spec = new ReservationSearchSpecification().sameMemberId(memberId).build();
val results: List<Reservation> = reservationRepository.findAll(spec)
// when
List<Reservation> found = reservationRepository.findAll(spec); assertSoftly(results) {
this shouldHaveSize 2
// then this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday)
assertThat(found).containsExactly(reservation1, reservation2, reservation3); }
} }
@Test "대기 상태인 예약을 조회한다" {
@DisplayName("동일한 시간의 예약을 찾는다.") val spec = ReservationSearchSpecification()
void searchByTimeId() { .waiting()
// given .build()
Long timeId = reservation1.getReservationTime().getId();
Specification<Reservation> spec = new ReservationSearchSpecification().sameTimeId(timeId).build(); val results: List<Reservation> = reservationRepository.findAll(spec)
// when assertSoftly(results) {
List<Reservation> found = reservationRepository.findAll(spec); this shouldHaveSize 1
this shouldContainExactly listOf(waitingTomorrow)
// then }
assertThat(found).containsExactly(reservation1, reservation2, reservation3); }
}
"예약 날짜가 오늘 이후인 예약을 조회한다" {
@Test val spec = ReservationSearchSpecification()
@DisplayName("동일한 날짜의 예약을 찾는다.") .dateStartFrom(LocalDate.now())
void searchByDate() { .build()
// given
LocalDate date = reservation1.getDate(); val results: List<Reservation> = reservationRepository.findAll(spec)
Specification<Reservation> spec = new ReservationSearchSpecification().sameDate(date).build();
assertSoftly(results) {
// when this shouldHaveSize 2
List<Reservation> found = reservationRepository.findAll(spec); this shouldContainExactly listOf(confirmedNow, waitingTomorrow)
}
// then }
assertThat(found).containsExactly(reservation1);
} "예약 날짜가 내일 이전인 예약을 조회한다" {
val spec = ReservationSearchSpecification()
@Test .dateEndAt(LocalDate.now().plusDays(1))
@DisplayName("확정 상태인 예약을 찾는다.") .build()
void searchConfirmedReservation() {
// given val results: List<Reservation> = reservationRepository.findAll(spec)
Specification<Reservation> spec = new ReservationSearchSpecification().confirmed().build();
assertSoftly(results) {
// when this shouldHaveSize 3
List<Reservation> found = reservationRepository.findAll(spec); this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
}
// then }
assertThat(found).containsExactly(reservation1, reservation2);
} beforeTest {
member = MemberFixture.create().also {
@Test entityManager.persist(it)
@DisplayName("대기 중인 예약을 찾는다.") }
void searchWaitingReservation() { reservationTime = ReservationTimeFixture.create().also {
// given entityManager.persist(it)
Specification<Reservation> spec = new ReservationSearchSpecification().waiting().build(); }
theme = ThemeFixture.create().also {
// when entityManager.persist(it)
List<Reservation> found = reservationRepository.findAll(spec); }
// then confirmedNow = ReservationFixture.create(
assertThat(found).containsExactly(reservation3); reservationTime = reservationTime,
} member = member,
theme = theme,
@Test date = LocalDate.now(),
@DisplayName("특정 날짜 이후의 예약을 찾는다.") status = ReservationStatus.CONFIRMED
void searchDateStartFrom() { ).also {
// given : 어제 이후의 예약을 조회하면, 모든 예약이 조회되어야 한다. entityManager.persist(it)
LocalDate date = LocalDate.now().minusDays(1L); }
Specification<Reservation> spec = new ReservationSearchSpecification().dateStartFrom(date).build();
confirmedNotPaidYesterday = ReservationFixture.create(
// when reservationTime = reservationTime,
List<Reservation> found = reservationRepository.findAll(spec); member = member,
theme = theme,
// then date = LocalDate.now().minusDays(1),
assertThat(found).containsExactly(reservation1, reservation2, reservation3); status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
} ).also {
entityManager.persist(it)
@Test }
@DisplayName("특정 날짜 이전의 예약을 찾는다.")
void searchDateEndAt() { waitingTomorrow = ReservationFixture.create(
// given : 내일 이전의 예약을 조회하면, 모든 예약이 조회되어야 한다. reservationTime = reservationTime,
LocalDate date = LocalDate.now().plusDays(1L); member = member,
Specification<Reservation> spec = new ReservationSearchSpecification().dateEndAt(date).build(); theme = theme,
date = LocalDate.now().plusDays(1),
// when status = ReservationStatus.WAITING
List<Reservation> found = reservationRepository.findAll(spec); ).also {
entityManager.persist(it)
// then }
assertThat(found).containsExactly(reservation1, reservation2, reservation3);
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 {