From 58b152f0d5b074139597e7ae6de54629a8beb9f0 Mon Sep 17 00:00:00 2001 From: Sumin Date: Wed, 17 Jun 2026 19:36:11 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=EB=8B=A8=EC=88=9C=20CRUD=20JPA?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../reservation/JpaReservationRepository.java | 41 ++++++++++++++ .../domain/reservation/Reservation.java | 45 +++++++++++++-- .../reservation/ReservationService.java | 55 +++++++++++-------- .../JpaReservationDateRepository.java | 7 +++ .../reservationdate/ReservationDate.java | 17 +++++- .../ReservationDateService.java | 8 +-- .../JpaReservationSlotRepository.java | 31 +++++++++++ .../reservationslot/ReservationSlot.java | 34 ++++++++++-- .../ReservationSlotService.java | 14 ++--- .../JpaReservationTimeRepository.java | 7 +++ .../reservationtime/ReservationTime.java | 19 ++++++- .../ReservationTimeService.java | 6 +- .../domain/theme/JpaThemeRepository.java | 7 +++ .../java/roomescape/domain/theme/Theme.java | 27 +++++++-- .../roomescape/domain/theme/ThemeService.java | 9 +-- .../domain/user/JpaUserRepository.java | 9 +++ .../java/roomescape/domain/user/User.java | 19 ++++++- .../roomescape/domain/user/UserService.java | 2 +- src/main/resources/application.yml | 8 +++ src/main/resources/data.sql | 22 ++++---- src/main/resources/schema.sql | 55 ------------------- 22 files changed, 314 insertions(+), 130 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/JpaReservationRepository.java create mode 100644 src/main/java/roomescape/domain/reservationdate/JpaReservationDateRepository.java create mode 100644 src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java create mode 100644 src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java create mode 100644 src/main/java/roomescape/domain/theme/JpaThemeRepository.java create mode 100644 src/main/java/roomescape/domain/user/JpaUserRepository.java delete mode 100644 src/main/resources/schema.sql diff --git a/build.gradle b/build.gradle index a2afec335e..f1ec74c4f5 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java new file mode 100644 index 0000000000..f211e7aef7 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java @@ -0,0 +1,41 @@ +package roomescape.domain.reservation; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface JpaReservationRepository extends JpaRepository { + + Optional findByIdAndStatusNot(Long id, ReservationStatus status); + + @Query(""" + select count(reservation) + from Reservation reservation + where reservation.reservationSlot.id = :reservationSlotId + and reservation.status <> :excludedStatus + """) + Long countActiveByReservationSlotId( + @Param("reservationSlotId") Long reservationSlotId, + @Param("excludedStatus") ReservationStatus excludedStatus + ); + + boolean existsByUserIdAndReservationSlotIdAndStatusNot( + Long userId, + Long reservationSlotId, + ReservationStatus status + ); + + @Query(""" + select reservation + from Reservation reservation + where reservation.reservationSlot.id = :reservationSlotId + and reservation.status <> :excludedStatus + order by reservation.updatedAt, reservation.id + """) + List findActiveReservationsInWaitingOrder( + @Param("reservationSlotId") Long reservationSlotId, + @Param("excludedStatus") ReservationStatus excludedStatus + ); +} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 9b2f26e7d2..45c2f6fbb8 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -1,22 +1,55 @@ package roomescape.domain.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import java.time.Clock; import java.time.LocalDateTime; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import roomescape.domain.reservationslot.ReservationSlot; import roomescape.domain.user.User; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.errors.ReservationSlotErrors; +@Table(name = "reservation") +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Reservation { - private final Long id; - private final ReservationSlot reservationSlot; - private final User user; - private final ReservationStatus status; - private final LocalDateTime createdAt; - private final LocalDateTime updatedAt; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "reservation_slot_id", nullable = false) + private ReservationSlot reservationSlot; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.VARCHAR) + @Column(nullable = false, columnDefinition = "VARCHAR(30)") + private ReservationStatus status; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; private Reservation( Long id, diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 8b56128306..49476c18c2 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -35,7 +35,8 @@ @RequiredArgsConstructor public class ReservationService { - private final ReservationRepository reservationRepository; + private final JpaReservationRepository reservationRepository; + private final ReservationRepository reservationQueryRepository; private final UserService userService; private final ReservationSlotService reservationSlotService; private final ThemeService themeService; @@ -58,23 +59,20 @@ public CreateReservationResponse createReservation(CreateReservationRequest requ } public List getAllReservations() { - return reservationRepository.findAll().stream() + return reservationQueryRepository.findAll().stream() .map(ReservationResponse::from) .toList(); } public UserReservationsResponse getUserReservations(String username) { - List userReservations = reservationRepository.findReservations(username); + List userReservations = reservationQueryRepository.findReservations(username); return UserReservationsResponse.of(username, userReservations); } @Transactional public void cancelReservationByAdmin(Long id) { Reservation reservation = findActiveReservationByIdOrThrow(id); - reservationRepository.update( - reservation.getId(), - reservation.update(ReservationStatus.CANCELED, clock) - ); + reservationRepository.save(reservation.update(ReservationStatus.CANCELED, clock)); if (reservation.getStatus() == ReservationStatus.CONFIRMED) { promoteFirstWaitingReservation(reservation.getReservationSlot()); } @@ -84,10 +82,7 @@ public void cancelReservationByAdmin(Long id) { public void cancelUserReservation(Long id) { Reservation reservation = findActiveReservationByIdOrThrow(id); validateReservationDeletionAllowed(reservation); - reservationRepository.update( - reservation.getId(), - reservation.update(ReservationStatus.CANCELED, clock) - ); + reservationRepository.save(reservation.update(ReservationStatus.CANCELED, clock)); if (reservation.getStatus() == ReservationStatus.CONFIRMED) { promoteFirstWaitingReservation(reservation.getReservationSlot()); } @@ -147,12 +142,12 @@ private boolean hasSameReservationSlot(ReservationSlot oldSlot, ReservationSlot } private void updateReservationWhenSameSlot(Reservation reservation, ReservationSlot currentReservationSlot) { - Long currentReservationCount = reservationRepository.countByReservationSlotId(currentReservationSlot.getId()); - ReservationStatus updatedStatus = decideWaitingStatus(currentReservationCount - 1); - reservationRepository.update( - reservation.getId(), - reservation.update(currentReservationSlot, updatedStatus, clock) + Long currentReservationCount = reservationRepository.countActiveByReservationSlotId( + currentReservationSlot.getId(), + ReservationStatus.CANCELED ); + ReservationStatus updatedStatus = decideWaitingStatus(currentReservationCount - 1); + reservationRepository.save(reservation.update(currentReservationSlot, updatedStatus, clock)); if (shouldPromoteNextWaitingReservation(reservation, updatedStatus)) { promoteFirstWaitingReservation(currentReservationSlot); } @@ -167,7 +162,10 @@ private void updateReservationWhenMovingSlot( ReservationSlot currentSlot, ReservationSlot updatedSlot ) { - Long currentReservationCount = reservationRepository.countByReservationSlotId(updatedSlot.getId()); + Long currentReservationCount = reservationRepository.countActiveByReservationSlotId( + updatedSlot.getId(), + ReservationStatus.CANCELED + ); ReservationStatus updatedStatus = decideWaitingStatus(currentReservationCount); Reservation reservationToSave = reservation.update( @@ -176,24 +174,29 @@ private void updateReservationWhenMovingSlot( clock ); - reservationRepository.update(reservation.getId(), reservationToSave); + reservationRepository.save(reservationToSave); if (reservation.getStatus() == ReservationStatus.CONFIRMED) { promoteFirstWaitingReservation(currentSlot); } } private void promoteFirstWaitingReservation(ReservationSlot reservationSlot) { - reservationRepository.findReservationsInWaitingOrder(reservationSlot.getId()).stream() + reservationRepository.findActiveReservationsInWaitingOrder( + reservationSlot.getId(), + ReservationStatus.CANCELED + ).stream() .filter(reservation -> reservation.getStatus() == ReservationStatus.WAITING) .findFirst() - .ifPresent(reservation -> reservationRepository.update( - reservation.getId(), + .ifPresent(reservation -> reservationRepository.save( reservation.update(ReservationStatus.CONFIRMED, clock) )); } private Reservation buildReservation(ReservationSlot reservationSlot, User user) { - Long currentReservationCount = reservationRepository.countByReservationSlotId(reservationSlot.getId()); + Long currentReservationCount = reservationRepository.countActiveByReservationSlotId( + reservationSlot.getId(), + ReservationStatus.CANCELED + ); ReservationStatus newReservationStatus = decideWaitingStatus(currentReservationCount); return Reservation.createWithoutId( reservationSlot, @@ -204,7 +207,11 @@ private Reservation buildReservation(ReservationSlot reservationSlot, User user) } private void validateNoDuplicateReservation(User user, ReservationSlot reservationSlot) { - if (reservationRepository.existsActiveByUserIdAndReservationId(user.getId(), reservationSlot.getId())) { + if (reservationRepository.existsByUserIdAndReservationSlotIdAndStatusNot( + user.getId(), + reservationSlot.getId(), + ReservationStatus.CANCELED + )) { throw new BadRequestException(ReservationSlotErrors.DUPLICATED_RESERVATION); } } @@ -259,7 +266,7 @@ private boolean isPastTimeToday( } private Reservation findActiveReservationByIdOrThrow(Long id) { - return reservationRepository.findActiveReservation(id) + return reservationRepository.findByIdAndStatusNot(id, ReservationStatus.CANCELED) .orElseThrow(() -> new NotFoundException(ReservationErrors.USER_RESERVATION_NOT_FOUND)); } } diff --git a/src/main/java/roomescape/domain/reservationdate/JpaReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/JpaReservationDateRepository.java new file mode 100644 index 0000000000..c799220197 --- /dev/null +++ b/src/main/java/roomescape/domain/reservationdate/JpaReservationDateRepository.java @@ -0,0 +1,7 @@ +package roomescape.domain.reservationdate; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaReservationDateRepository extends JpaRepository { + +} diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDate.java b/src/main/java/roomescape/domain/reservationdate/ReservationDate.java index 4e8477b4a3..16bd5053c3 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDate.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDate.java @@ -1,13 +1,26 @@ package roomescape.domain.reservationdate; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.time.LocalDate; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ReservationDate { - private final Long id; - private final LocalDate date; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate date; public ReservationDate(Long id, LocalDate date) { this.id = id; diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java index 07b9f0ef7c..46f7c604a2 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java @@ -7,7 +7,7 @@ import roomescape.domain.reservationdate.admin.dto.CreateReservationDateRequest; import roomescape.domain.reservationdate.admin.dto.CreateReservationDateResponse; import roomescape.domain.reservationdate.dto.ReservationDateResponse; -import roomescape.domain.reservationslot.ReservationSlotRepository; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.support.exception.ConflictException; import roomescape.support.exception.NotFoundException; import roomescape.support.exception.errors.ReservationDateErrors; @@ -16,8 +16,8 @@ @RequiredArgsConstructor public class ReservationDateService { - private final ReservationSlotRepository reservationSlotRepository; - private final ReservationDateRepository reservationDateRepository; + private final JpaReservationSlotRepository reservationSlotRepository; + private final JpaReservationDateRepository reservationDateRepository; public List getAllReservationDateForAdmin() { return reservationDateRepository.findAll().stream() @@ -31,7 +31,7 @@ public CreateReservationDateResponse createReservationDate(CreateReservationDate } public void deleteReservationDate(Long id) { - if (reservationSlotRepository.countByReservationDateId(id) > 0) { + if (reservationSlotRepository.countByDateId(id) > 0) { throw new ConflictException(ReservationDateErrors.RESERVATION_DATE_IN_USE); } reservationDateRepository.deleteById(id); diff --git a/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java b/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java new file mode 100644 index 0000000000..64b6d28e7a --- /dev/null +++ b/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java @@ -0,0 +1,31 @@ +package roomescape.domain.reservationslot; + +import jakarta.persistence.LockModeType; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface JpaReservationSlotRepository extends JpaRepository { + + int countByTimeId(Long id); + + int countByDateId(Long id); + + int countByThemeId(Long id); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + select reservationSlot + from ReservationSlot reservationSlot + where reservationSlot.time.id = :timeId + and reservationSlot.date.id = :dateId + and reservationSlot.theme.id = :themeId + """) + Optional findByScheduleToUpdate( + @Param("timeId") Long timeId, + @Param("dateId") Long dateId, + @Param("themeId") Long themeId + ); +} diff --git a/src/main/java/roomescape/domain/reservationslot/ReservationSlot.java b/src/main/java/roomescape/domain/reservationslot/ReservationSlot.java index 7639f3a9a5..1c131953e4 100644 --- a/src/main/java/roomescape/domain/reservationslot/ReservationSlot.java +++ b/src/main/java/roomescape/domain/reservationslot/ReservationSlot.java @@ -1,6 +1,16 @@ package roomescape.domain.reservationslot; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; @@ -9,13 +19,29 @@ import roomescape.support.exception.errors.ReservationTimeErrors; import roomescape.support.exception.errors.ThemeErrors; +@Table( + uniqueConstraints = @UniqueConstraint(columnNames = {"date_id", "time_id", "theme_id"}) +) +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ReservationSlot { - private final Long id; - private final ReservationDate date; - private final ReservationTime time; - private final Theme theme; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "date_id", nullable = false) + private ReservationDate date; + + @ManyToOne + @JoinColumn(name = "time_id", nullable = false) + private ReservationTime time; + + @ManyToOne + @JoinColumn(name = "theme_id", nullable = false) + private Theme theme; private ReservationSlot( Long id, diff --git a/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java b/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java index 5613928902..3163d841bd 100644 --- a/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java +++ b/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java @@ -2,16 +2,16 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.dto.ReservationCountResult; import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationdate.ReservationDateRepository; +import roomescape.domain.reservationdate.JpaReservationDateRepository; import roomescape.domain.reservationslot.dto.ReservationSlotResponse; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeRepository; +import roomescape.domain.theme.JpaThemeRepository; import roomescape.support.exception.NotFoundException; import roomescape.support.exception.errors.ReservationDateErrors; import roomescape.support.exception.errors.ReservationSlotErrors; @@ -21,9 +21,9 @@ @RequiredArgsConstructor public class ReservationSlotService { - private final ReservationSlotRepository reservationSlotRepository; - private final ThemeRepository themeRepository; - private final ReservationDateRepository reservationDateRepository; + private final JpaReservationSlotRepository reservationSlotRepository; + private final JpaThemeRepository themeRepository; + private final JpaReservationDateRepository reservationDateRepository; private final ReservationRepository reservationRepository; public List getReservationSlots(Long themeId, Long dateId) { @@ -46,7 +46,7 @@ public ReservationSlot findOrCreateReservationSlot( reservationDate.getId(), theme.getId() ).orElseGet(() -> saveReservationSlot(reservationDate, reservationTime, theme)); - } catch (DuplicateKeyException e) { + } catch (DataIntegrityViolationException e) { return reservationSlotRepository.findByScheduleToUpdate( reservationTime.getId(), reservationDate.getId(), diff --git a/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java new file mode 100644 index 0000000000..07e64f5612 --- /dev/null +++ b/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java @@ -0,0 +1,7 @@ +package roomescape.domain.reservationtime; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaReservationTimeRepository extends JpaRepository { + +} diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java index 13356a52aa..d793c4153d 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java @@ -1,15 +1,30 @@ package roomescape.domain.reservationtime; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.LocalTime; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.errors.ReservationTimeErrors; +@Table(name = "reservation_time") +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ReservationTime { - private final Long id; - private final LocalTime startAt; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalTime startAt; private ReservationTime(Long id, LocalTime startAt) { validate(startAt); diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index 04165fe71a..59715ba783 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -3,7 +3,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import roomescape.domain.reservationslot.ReservationSlotRepository; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.reservationtime.admin.dto.CreateTimeRequest; import roomescape.domain.reservationtime.admin.dto.CreateTimeResponse; import roomescape.domain.reservationtime.admin.dto.ReservationTimeResponse; @@ -15,8 +15,8 @@ @RequiredArgsConstructor public class ReservationTimeService { - private final ReservationTimeRepository reservationTimeRepository; - private final ReservationSlotRepository reservationSlotRepository; + private final JpaReservationTimeRepository reservationTimeRepository; + private final JpaReservationSlotRepository reservationSlotRepository; public CreateTimeResponse createReservationTime(CreateTimeRequest request) { ReservationTime reservationTime = reservationTimeRepository.save(request.toEntity()); diff --git a/src/main/java/roomescape/domain/theme/JpaThemeRepository.java b/src/main/java/roomescape/domain/theme/JpaThemeRepository.java new file mode 100644 index 0000000000..d9f053e2a3 --- /dev/null +++ b/src/main/java/roomescape/domain/theme/JpaThemeRepository.java @@ -0,0 +1,7 @@ +package roomescape.domain.theme; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaThemeRepository extends JpaRepository { + +} diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 26f9e21328..00f640b5aa 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -1,18 +1,37 @@ package roomescape.domain.theme; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.errors.ThemeErrors; +@Table(name = "theme") +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Theme { private static final int MAX_NAME_LENGTH = 10; - private final Long id; - private final String name; - private final String content; - private final String url; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private String url; private Theme(Long id, String name, String content, String url) { validateName(name); diff --git a/src/main/java/roomescape/domain/theme/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index 44af704187..cfb5031542 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -5,7 +5,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import roomescape.domain.reservationslot.ReservationSlotRepository; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.theme.admin.dto.AdminThemeResponse; import roomescape.domain.theme.admin.dto.CreateThemeRequest; import roomescape.domain.theme.admin.dto.CreateThemeResponse; @@ -23,8 +23,9 @@ public class ThemeService { private static final int RANK_LIMIT = 10; private static final int RANK_DAYS_LIMIT = 7; - private final ThemeRepository themeRepository; - private final ReservationSlotRepository reservationSlotRepository; + private final JpaThemeRepository themeRepository; + private final JpaReservationSlotRepository reservationSlotRepository; + private final ThemeRepository themeQueryRepository; private final Clock clock; @@ -55,7 +56,7 @@ public List getAllTheme() { public List getThemeRank() { LocalDate today = LocalDate.now(clock); LocalDate startDay = today.minusDays(RANK_DAYS_LIMIT); - List popularThemes = themeRepository.findPopularThemes(RANK_LIMIT, startDay, today); + List popularThemes = themeQueryRepository.findPopularThemes(RANK_LIMIT, startDay, today); return popularThemes.stream() .map(ThemeRankResponse::from) .toList(); diff --git a/src/main/java/roomescape/domain/user/JpaUserRepository.java b/src/main/java/roomescape/domain/user/JpaUserRepository.java new file mode 100644 index 0000000000..4056491569 --- /dev/null +++ b/src/main/java/roomescape/domain/user/JpaUserRepository.java @@ -0,0 +1,9 @@ +package roomescape.domain.user; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaUserRepository extends JpaRepository { + + Optional findByName(String name); +} diff --git a/src/main/java/roomescape/domain/user/User.java b/src/main/java/roomescape/domain/user/User.java index f4048960d6..cd99653416 100644 --- a/src/main/java/roomescape/domain/user/User.java +++ b/src/main/java/roomescape/domain/user/User.java @@ -1,16 +1,31 @@ package roomescape.domain.user; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.errors.UserErrors; +@Table(name = "users") +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User { private static final int MAX_NAME_LENGTH = 10; - private final Long id; - private final String name; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; private User(Long id, String name) { validate(name); diff --git a/src/main/java/roomescape/domain/user/UserService.java b/src/main/java/roomescape/domain/user/UserService.java index 1de3470cd2..52d8b24f59 100644 --- a/src/main/java/roomescape/domain/user/UserService.java +++ b/src/main/java/roomescape/domain/user/UserService.java @@ -7,7 +7,7 @@ @RequiredArgsConstructor public class UserService { - private final UserRepository userRepository; + private final JpaUserRepository userRepository; public User findOrCreateUser(String name) { return userRepository.findByName(name) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 46defae0db..441388cfb5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,5 +5,13 @@ spring: path: /h2-console datasource: url: jdbc:h2:mem:database + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create-drop + defer-datasource-initialization: true token: "" diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 35dab549e3..4a77e6e855 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -6,17 +6,17 @@ VALUES ('10:00'), ('16:00'); INSERT INTO reservation_date (date) -VALUES ('2026-05-30'), - ('2026-05-31'), - ('2026-06-01'), - ('2026-06-02'), - ('2026-06-03'), - ('2026-06-04'), - ('2026-06-05'), - ('2026-06-06'), - ('2026-06-07'), - ('2026-06-08'), - ('2026-06-09'); +VALUES ('2026-06-18'), + ('2026-06-19'), + ('2026-06-20'), + ('2026-06-21'), + ('2026-06-22'), + ('2026-06-23'), + ('2026-06-24'), + ('2026-06-25'), + ('2026-06-26'), + ('2026-06-27'), + ('2026-06-28'); INSERT INTO theme (name, content, url) VALUES ('공포', '오금이 저리는 공포입니다.', '/themes/scary'), diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index 25b2dca1c1..0000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,55 +0,0 @@ -CREATE TABLE reservation_time -( - id BIGINT NOT NULL AUTO_INCREMENT, - start_at TIME NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE reservation_date -( - id BIGINT NOT NULL AUTO_INCREMENT, - date DATE NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE theme -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - content VARCHAR(255) NOT NULL, - url VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE users -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL UNIQUE, - PRIMARY KEY (id) -); - -CREATE TABLE reservation_slot -( - id BIGINT NOT NULL AUTO_INCREMENT, - date_id BIGINT NOT NULL, - time_id BIGINT NOT NULL, - theme_id BIGINT NOT NULL, - PRIMARY KEY (id), - UNIQUE (date_id, time_id, theme_id), - FOREIGN KEY (time_id) REFERENCES reservation_time (id), - FOREIGN KEY (date_id) REFERENCES reservation_date (id), - FOREIGN KEY (theme_id) REFERENCES theme (id) -); - -CREATE TABLE reservation -( - id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - reservation_slot_id BIGINT NOT NULL, - status VARCHAR(30) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - FOREIGN KEY (user_id) REFERENCES users (id), - FOREIGN KEY (reservation_slot_id) REFERENCES reservation_slot (id) -); From ef71728bb2533338c3f5023e6b0ca7d7826a02b5 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 18 Jun 2026 10:43:15 +0900 Subject: [PATCH 2/6] =?UTF-8?q?test:=20fake=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20mock=20=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationService.java | 19 +- .../reservation/ReservationServiceTest.java | 1052 +++-------------- .../ReservationDateServiceTest.java | 79 +- .../ReservationSlotServiceTest.java | 94 +- .../ReservationTimeServiceTest.java | 76 +- .../domain/theme/ThemeServiceTest.java | 70 +- .../fake/FakeReservationDateRepository.java | 55 - .../fake/FakeReservationRepository.java | 146 --- .../fake/FakeReservationSlotRepository.java | 103 -- .../fake/FakeReservationTimeRepository.java | 55 - .../support/fake/FakeThemeRepository.java | 54 - .../support/fake/FakeUserRepository.java | 45 - src/test/resources/application.yml | 6 + 13 files changed, 322 insertions(+), 1532 deletions(-) delete mode 100644 src/test/java/roomescape/support/fake/FakeReservationDateRepository.java delete mode 100644 src/test/java/roomescape/support/fake/FakeReservationRepository.java delete mode 100644 src/test/java/roomescape/support/fake/FakeReservationSlotRepository.java delete mode 100644 src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java delete mode 100644 src/test/java/roomescape/support/fake/FakeThemeRepository.java delete mode 100644 src/test/java/roomescape/support/fake/FakeUserRepository.java diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 49476c18c2..b412165712 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -72,8 +72,9 @@ public UserReservationsResponse getUserReservations(String username) { @Transactional public void cancelReservationByAdmin(Long id) { Reservation reservation = findActiveReservationByIdOrThrow(id); + ReservationStatus previousStatus = reservation.getStatus(); reservationRepository.save(reservation.update(ReservationStatus.CANCELED, clock)); - if (reservation.getStatus() == ReservationStatus.CONFIRMED) { + if (previousStatus == ReservationStatus.CONFIRMED) { promoteFirstWaitingReservation(reservation.getReservationSlot()); } } @@ -82,8 +83,9 @@ public void cancelReservationByAdmin(Long id) { public void cancelUserReservation(Long id) { Reservation reservation = findActiveReservationByIdOrThrow(id); validateReservationDeletionAllowed(reservation); + ReservationStatus previousStatus = reservation.getStatus(); reservationRepository.save(reservation.update(ReservationStatus.CANCELED, clock)); - if (reservation.getStatus() == ReservationStatus.CONFIRMED) { + if (previousStatus == ReservationStatus.CONFIRMED) { promoteFirstWaitingReservation(reservation.getReservationSlot()); } } @@ -147,14 +149,18 @@ private void updateReservationWhenSameSlot(Reservation reservation, ReservationS ReservationStatus.CANCELED ); ReservationStatus updatedStatus = decideWaitingStatus(currentReservationCount - 1); + ReservationStatus previousStatus = reservation.getStatus(); reservationRepository.save(reservation.update(currentReservationSlot, updatedStatus, clock)); - if (shouldPromoteNextWaitingReservation(reservation, updatedStatus)) { + if (shouldPromoteNextWaitingReservation(previousStatus, updatedStatus)) { promoteFirstWaitingReservation(currentReservationSlot); } } - private boolean shouldPromoteNextWaitingReservation(Reservation reservation, ReservationStatus updatedStatus) { - return reservation.getStatus() == ReservationStatus.CONFIRMED && updatedStatus == ReservationStatus.WAITING; + private boolean shouldPromoteNextWaitingReservation( + ReservationStatus previousStatus, + ReservationStatus updatedStatus + ) { + return previousStatus == ReservationStatus.CONFIRMED && updatedStatus == ReservationStatus.WAITING; } private void updateReservationWhenMovingSlot( @@ -174,8 +180,9 @@ private void updateReservationWhenMovingSlot( clock ); + ReservationStatus previousStatus = reservation.getStatus(); reservationRepository.save(reservationToSave); - if (reservation.getStatus() == ReservationStatus.CONFIRMED) { + if (previousStatus == ReservationStatus.CONFIRMED) { promoteFirstWaitingReservation(currentSlot); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index 3fcbc7329f..13132019af 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.BDDAssertions.tuple; import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.time.Clock; @@ -10,460 +9,109 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import roomescape.domain.reservation.admin.dto.ReservationResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; -import roomescape.domain.reservation.dto.ReservationWithWaitingNumber; import roomescape.domain.reservation.dto.UpdateReservationRequest; -import roomescape.domain.reservation.dto.UserReservationsResponse; +import roomescape.domain.reservationdate.JpaReservationDateRepository; import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationdate.ReservationDateService; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.reservationslot.ReservationSlot; -import roomescape.domain.reservationslot.ReservationSlotService; +import roomescape.domain.reservationtime.JpaReservationTimeRepository; import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.reservationtime.ReservationTimeService; +import roomescape.domain.theme.JpaThemeRepository; import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeService; +import roomescape.domain.user.JpaUserRepository; import roomescape.domain.user.User; -import roomescape.domain.user.UserService; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.RoomescapeException; -import roomescape.support.fake.FakeReservationDateRepository; -import roomescape.support.fake.FakeReservationRepository; -import roomescape.support.fake.FakeReservationSlotRepository; -import roomescape.support.fake.FakeReservationTimeRepository; -import roomescape.support.fake.FakeThemeRepository; -import roomescape.support.fake.FakeUserRepository; +@SpringBootTest class ReservationServiceTest { - private static final ZoneId ZONE_ID = ZoneId.systemDefault(); - private FakeReservationSlotRepository reservationSlotRepository; - private FakeReservationTimeRepository reservationTimeRepository; - private FakeReservationDateRepository reservationDateRepository; - private FakeThemeRepository themeRepository; - private FakeUserRepository userRepository; - private FakeReservationRepository reservationRepository; + @Autowired + private ReservationService reservationService; - @BeforeEach - void setUp() { - reservationSlotRepository = new FakeReservationSlotRepository(); - reservationTimeRepository = new FakeReservationTimeRepository(); - reservationDateRepository = new FakeReservationDateRepository(); - themeRepository = new FakeThemeRepository(); - userRepository = new FakeUserRepository(); - reservationRepository = new FakeReservationRepository(); - } - - @Test - @DisplayName("존재하는 예약 시간으로 예약을 생성한다.") - void createReservationWithExistingReservationTime() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); - ReservationTime savedReservationTime = reservationTimeRepository.save(reservationTime); - ReservationDate reservationDate = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)); - ReservationDate savedReservationDate = reservationDateRepository.save(reservationDate); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - savedReservationDate.getId(), - savedReservationTime.getId(), - theme.getId() - ); - - // when - CreateReservationResponse response = reservationService.createReservation(request); - - // then - assertSoftly(softly -> { - assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 13)); - assertThat(response.time()).isEqualTo(LocalTime.of(10, 0)); - assertThat(response.theme().name()).isEqualTo("공포"); - }); - } - - @Test - @DisplayName("존재하지 않는 예약 시간으로 예약을 생성하면 예외가 발생한다.") - void throwExceptionWhenCreatingReservationWithNonExistentReservationTime() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest("보예", 1L, 1L, 1L); - - // when & then - assertThatThrownBy(() -> reservationService.createReservation(request)) - .isInstanceOf(RoomescapeException.class) - .hasMessage("존재하지 않는 예약 시간대 입니다."); - } - - @Test - @DisplayName("존재하지 않는 테마로 예약을 생성하면 예외가 발생한다.") - void throwExceptionWhenCreatingReservationWithNonExistentTheme() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0))); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13))); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - reservationDate.getId(), - reservationTime.getId(), - 3L - ); - - // when & then - assertThatThrownBy(() -> reservationService.createReservation(request)) - .isInstanceOf(RoomescapeException.class) - .hasMessage("존재하지 않는 테마 입니다."); - } - - @Test - @DisplayName("예약 목록을 전체 조회한다.") - void getAllReservations() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationDate savedReservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운테마", "theme-url")); - ReservationSlot reservationSlot = reservationSlotRepository.save( - ReservationSlot.createWithoutId(savedReservationDate, reservationTime, theme) - ); - User user = userRepository.save(User.createWithoutId("보예")); - reservationRepository.save( - Reservation.createWithoutId( - reservationSlot, - user, - ReservationStatus.CONFIRMED, - now - ) - ); - - ReservationService reservationService = createReservationService(now); - - // when - List responses = reservationService.getAllReservations(); - - // then - assertSoftly(softly -> { - assertThat(responses).hasSize(1); - assertThat(responses.getFirst().date()).isEqualTo(LocalDate.of(2026, 5, 13)); - assertThat(responses.getFirst().time().id()).isEqualTo(reservationTime.getId()); - assertThat(responses.getFirst().time().startAt()).isEqualTo(LocalTime.of(10, 0)); - assertThat(responses.getFirst().theme().id()).isEqualTo(theme.getId()); - assertThat(responses.getFirst().theme().name()).isEqualTo("공포"); - }); - } - - @Test - @DisplayName("관리자는 취소된 예약을 포함한 예약 목록을 전체 조회한다.") - void getAllReservationsIncludingCanceledReservation() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationDate savedReservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운테마", "theme-url")); - ReservationSlot reservationSlot = reservationSlotRepository.save( - ReservationSlot.createWithoutId(savedReservationDate, reservationTime, theme) - ); - User confirmedUser = userRepository.save(User.createWithoutId("보예")); - User canceledUser = userRepository.save(User.createWithoutId("수민")); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - confirmedUser, - ReservationStatus.CONFIRMED, - now - )); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - canceledUser, - ReservationStatus.CANCELED, - now - )); - ReservationService reservationService = createReservationService(now); + @Autowired + private JpaReservationRepository reservationRepository; - // when - List responses = reservationService.getAllReservations(); + @Autowired + private JpaReservationSlotRepository reservationSlotRepository; - // then - assertThat(responses) - .extracting(ReservationResponse::userName, ReservationResponse::reservationStatus) - .containsExactly( - tuple("보예", ReservationStatus.CONFIRMED), - tuple("수민", ReservationStatus.CANCELED) - ); - } + @Autowired + private JpaUserRepository userRepository; - @Test - @DisplayName("사용자가 이름으로 예약을 조회한다.") - void getUserReservationsByName() { - // given - String name = "보예짱"; - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationDate firstReservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationDate secondReservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 14)) - ); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운테마", "theme-url")); - User user = userRepository.save(User.createWithoutId(name)); - ReservationSlot secondReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(secondReservationDate, - reservationTime, - theme - ) - ); - ReservationSlot firstReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(firstReservationDate, - reservationTime, - theme - ) - ); - reservationRepository.save(Reservation.createWithoutId( - secondReservation, - user, - ReservationStatus.CONFIRMED, - now - )); - reservationRepository.save(Reservation.createWithoutId( - firstReservation, - user, - ReservationStatus.CONFIRMED, - now - )); - ReservationService reservationService = createReservationService(now); + @Autowired + private JpaReservationDateRepository reservationDateRepository; - // when - UserReservationsResponse userReservations = reservationService.getUserReservations(name); - - // then - assertSoftly(softly -> { - assertThat(userReservations.reservations()).hasSize(2); - assertThat(userReservations.username()).isEqualTo("보예짱"); - assertThat(userReservations.reservations()) - .extracting( - "reservationSlot.date.startWhen", - "reservationSlot.time.startAt", - "reservationSlot.theme.name", - "status" - ) - .containsExactly( - tuple(LocalDate.of(2026, 5, 13), LocalTime.of(10, 0), "공포", "CONFIRMED"), - tuple(LocalDate.of(2026, 5, 14), LocalTime.of(10, 0), "공포", "CONFIRMED") - ); - }); - } + @Autowired + private JpaReservationTimeRepository reservationTimeRepository; - @Test - @DisplayName("사용자는 취소된 예약을 포함한 본인의 예약 목록을 조회한다.") - void getUserReservationsIncludingCanceledReservation() { - // given - String name = "보예짱"; - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운테마", "theme-url")); - User user = userRepository.save(User.createWithoutId(name)); - ReservationSlot reservationSlot = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - user, - ReservationStatus.CONFIRMED, - now - )); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - user, - ReservationStatus.WAITING, - now - )); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - user, - ReservationStatus.CANCELED, - now - )); - ReservationService reservationService = createReservationService(now); + @Autowired + private JpaThemeRepository themeRepository; - // when - UserReservationsResponse userReservations = reservationService.getUserReservations(name); + @TestConfiguration + static class TestClockConfig { - // then - assertThat(userReservations.reservations()) - .extracting("status", "waitingNumber") - .containsExactlyInAnyOrder( - tuple("CANCELED", null), - tuple("WAITING", 1L), - tuple("CONFIRMED", null) + @Bean + @Primary + Clock fixedClock() { + ZoneId zoneId = ZoneId.systemDefault(); + return Clock.fixed( + LocalDateTime.of(2026, 5, 31, 13, 0) + .atZone(zoneId) + .toInstant(), + zoneId ); + } } - @Test - @DisplayName("오늘보다 이전 날짜는 예약할 수 없다.") - void throwExceptionWhenCreatingReservationBeforeToday() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(14, 0)) - ); - ReservationDate beforeToday = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 10)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - beforeToday.getId(), - reservationTime.getId(), - theme.getId() - ); - - // when & then - assertThatThrownBy(() -> reservationService.createReservation(request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); - } - - @Test - @DisplayName("오늘 예약일 경우 현재 시간 이전은 예약할 수 없다.") - void throwExceptionWhenCreatingReservationBeforeCurrentTimeOnToday() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime beforeNow = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(12, 59)) - ); - ReservationDate today = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - today.getId(), - beforeNow.getId(), - theme.getId() - ); - - // when & then - assertThatThrownBy(() -> reservationService.createReservation(request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("예약 시간은 현재 이후여야 합니다. 현재 시각:" + LocalTime.of(13, 0)); - } - - @Test - @DisplayName("오늘 예약이지만 현재 시간은 예약할 수 있다.") - void createReservationAtCurrentTimeOnToday() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime nowTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(13, 0)) - ); - ReservationDate today = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - today.getId(), - nowTime.getId(), - theme.getId() - ); - - // when - CreateReservationResponse response = reservationService.createReservation(request); - ReservationSlot reservation = reservationSlotRepository.findById(response.id()).orElseThrow(); - - // then - assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(reservation.getId()); - assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 12)); - assertThat(response.time()).isEqualTo(LocalTime.of(13, 0)); - assertThat(response.theme().name()).isEqualTo("공포"); - assertThat(response.theme().content()).isEqualTo("무서운 테마"); - assertThat(response.theme().url()).isEqualTo("theme-url"); - } - ); + @BeforeEach + void setUp() { + reservationRepository.deleteAllInBatch(); + reservationSlotRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + reservationDateRepository.deleteAllInBatch(); + reservationTimeRepository.deleteAllInBatch(); + themeRepository.deleteAllInBatch(); } @Test - @DisplayName("날짜가 오늘 이후이고 현재 시간보다 이전이면 정상 예약 된다.") - void createReservationAfterTodayEvenIfTimeIsBeforeNow() { + @DisplayName("예약을 생성한다.") + void createReservation() { // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationDate today = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - today.getId(), - reservationTime.getId(), - theme.getId() - ); + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-06-01"); + Long timeId = saveTime("10:00"); + CreateReservationRequest request = new CreateReservationRequest("보예", dateId, timeId, themeId); // when CreateReservationResponse response = reservationService.createReservation(request); - Reservation reservation = reservationRepository.findActiveReservation(response.id()).orElseThrow(); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(reservation.getId()); - assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 13)); - assertThat(response.time()).isEqualTo(LocalTime.of(10, 0)); - assertThat(response.theme().name()).isEqualTo("공포"); - assertThat(response.theme().content()).isEqualTo("무서운 테마"); - assertThat(response.theme().url()).isEqualTo("theme-url"); - } - ); + assertThat(response.date()).isEqualTo(LocalDate.of(2026, 6, 1)); + assertThat(response.time()).isEqualTo(LocalTime.of(10, 0)); + assertThat(response.theme().name()).isEqualTo("공포"); + assertThat(findReservationStatus(response.id())).isEqualTo(ReservationStatus.CONFIRMED); + }); } @Test - @DisplayName("중복된 예약은 예외가 발생한다.") + @DisplayName("중복된 예약은 생성할 수 없다.") void throwExceptionWhenCreatingDuplicatedReservation() { // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); - ReservationTime savedReservationTime = reservationTimeRepository.save(reservationTime); - ReservationDate reservationDate = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)); - ReservationDate savedReservationDate = reservationDateRepository.save(reservationDate); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationService reservationService = createReservationService(now); - CreateReservationRequest request = new CreateReservationRequest( - "보예", - savedReservationDate.getId(), - savedReservationTime.getId(), - theme.getId() - ); + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-06-01"); + Long timeId = saveTime("10:00"); + CreateReservationRequest request = new CreateReservationRequest("보예", dateId, timeId, themeId); reservationService.createReservation(request); // when & then @@ -473,352 +121,80 @@ void throwExceptionWhenCreatingDuplicatedReservation() { } @Test - @DisplayName("사용자는 미래 예약을 삭제할 수 있다.") - void deleteFutureReservationForUser() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - - // when - reservationService.cancelUserReservation(savedUserReservation.getId()); - - // then - assertThat(reservationRepository.findActiveReservation(savedUserReservation.getId())).isEmpty(); - } - - @Test - @DisplayName("사용자는 이미 시간이 지난 예약을 삭제할 수 없다.") - void throwExceptionWhenUserDeletesPastTimeReservation() { + @DisplayName("오늘보다 이전 날짜는 예약할 수 없다.") + void throwExceptionWhenCreatingReservationBeforeToday() { // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(12, 59)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-05-30"); + Long timeId = saveTime("10:00"); + CreateReservationRequest request = new CreateReservationRequest("보예", dateId, timeId, themeId); // when & then - assertThatThrownBy(() -> reservationService.cancelUserReservation(savedUserReservation.getId())) + assertThatThrownBy(() -> reservationService.createReservation(request)) .isInstanceOf(BadRequestException.class) - .hasMessage("현재보다 이전 시간 예약을 삭제할 수 없습니다. 현재 시각:" + LocalTime.of(13, 0)); + .hasMessage("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:" + LocalDate.of(2026, 5, 31)); } @Test - @DisplayName("사용자는 이미 날짜가 지난 예약을 삭제할 수 없다.") - void throwExceptionWhenUserDeletesPastDateReservation() { + @DisplayName("오늘 예약일 경우 현재 시간 이전은 예약할 수 없다.") + void throwExceptionWhenCreatingReservationBeforeCurrentTimeOnToday() { // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(12, 59)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 11)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-05-31"); + Long timeId = saveTime("12:59"); + CreateReservationRequest request = new CreateReservationRequest("보예", dateId, timeId, themeId); // when & then - assertThatThrownBy(() -> reservationService.cancelUserReservation(savedUserReservation.getId())) + assertThatThrownBy(() -> reservationService.createReservation(request)) .isInstanceOf(BadRequestException.class) - .hasMessage("예전 예약은 삭제할 수 없습니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); - } - - @Test - @DisplayName("사용자가 존재하지 않는 예약을 삭제하면 예외가 발생한다.") - void throwExceptionWhenUserDeletesNonExistentReservation() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationService reservationService = createReservationService(now); - - // when & then - assertThatThrownBy(() -> reservationService.cancelUserReservation(1L)) - .isInstanceOf(RoomescapeException.class) - .hasMessage("사용자 예약 신청이 존재하지 않습니다."); - } - - @Test - @DisplayName("확정 예약을 취소하면 슬롯의 활성 예약은 0건이다.") - void cancelConfirmedReservation() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save( - Theme.createWithoutId("공포", "무서운 테마", "theme-url") - ); - ReservationSlot reservationSlot = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - User user = userRepository.save(User.createWithoutId("boye")); - Reservation confirmedReservation = saveConfirmedReservation(reservationSlot, user, now); - ReservationService reservationService = createReservationService(now); - - // when - reservationService.cancelUserReservation(confirmedReservation.getId()); - - // then - assertSoftly(softly -> { - assertThat(reservationSlotRepository.findBySchedule( - reservationTime.getId(), - reservationDate.getId(), - theme.getId()) - ).isNotEmpty(); - assertThat(reservationRepository.countByReservationSlotId(reservationSlot.getId())).isZero(); - assertThat(reservationRepository.findActiveReservation(confirmedReservation.getId())).isEmpty(); - } - ); + .hasMessage("예약 시간은 현재 이후여야 합니다. 현재 시각:" + LocalTime.of(13, 0)); } @Test - @DisplayName("확정 예약자가 예약을 취소하면 대기 1번 예약자만 확정으로 변경된다.") + @DisplayName("확정 예약자가 예약을 취소하면 첫 번째 대기 예약자가 확정된다.") void promoteFirstWaitingReservation() { // given - Clock confirmedClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - Clock firstWaitingClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 1)); - Clock secondWaitingClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 2)); - Clock cancelClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 3)); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - Theme theme = themeRepository.save( - Theme.createWithoutId("공포", "무섭다", "theme-url") - ); - ReservationSlot reservationSlot = reservationSlotRepository.save(ReservationSlot.createWithoutId( - reservationDate, reservationTime, theme - )); - User confirmedUser = userRepository.save(User.createWithoutId("보예")); - User firstWaitingUser = userRepository.save(User.createWithoutId("수민")); - User secondWaitingUser = userRepository.save(User.createWithoutId("말랑")); - saveConfirmedReservation(reservationSlot, confirmedUser, confirmedClock); - saveWaitingReservation(reservationSlot, firstWaitingUser, firstWaitingClock); - saveWaitingReservation(reservationSlot, secondWaitingUser, secondWaitingClock); + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-06-01"); + Long timeId = saveTime("10:00"); + Long slotId = saveReservationSlot(dateId, timeId, themeId); + Long confirmedReservationId = saveReservation("보예", slotId, ReservationStatus.CONFIRMED, "2026-05-31 13:00:00"); + Long firstWaitingReservationId = saveReservation("수민", slotId, ReservationStatus.WAITING, "2026-05-31 13:01:00"); + Long secondWaitingReservationId = saveReservation("말랑", slotId, ReservationStatus.WAITING, "2026-05-31 13:02:00"); // when - ReservationService reservationService = createReservationService(cancelClock); - reservationService.cancelUserReservation(confirmedUser.getId()); + reservationService.cancelUserReservation(confirmedReservationId); // then - ReservationWithWaitingNumber updatedReservation = reservationRepository.findReservations("수민").getFirst(); - ReservationWithWaitingNumber waitingReservation = reservationRepository.findReservations("말랑").getFirst(); assertSoftly(softly -> { - assertThat(updatedReservation.reservation().getStatus()).isEqualTo(ReservationStatus.CONFIRMED); - assertThat(waitingReservation.reservation().getStatus()).isEqualTo(ReservationStatus.WAITING); - assertThat(waitingReservation.waitingNumber()).isEqualTo(1L); + assertThat(findReservationStatus(confirmedReservationId)).isEqualTo(ReservationStatus.CANCELED); + assertThat(findReservationStatus(firstWaitingReservationId)).isEqualTo(ReservationStatus.CONFIRMED); + assertThat(findReservationStatus(secondWaitingReservationId)).isEqualTo(ReservationStatus.WAITING); }); } - @Test - @DisplayName("대기 예약자가 대기를 취소한다면 다른 예약이 확정으로 승격되지 않는다.") - void notPromoteFirstWaitingReservation() { - // given - Clock confirmedClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - Clock firstWaitingClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 1)); - Clock secondWaitingClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 2)); - Clock cancelClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 3)); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - Theme theme = themeRepository.save( - Theme.createWithoutId("공포", "무섭다", "theme-url") - ); - ReservationSlot reservationSlot = reservationSlotRepository.save(ReservationSlot.createWithoutId( - reservationDate, reservationTime, theme - )); - User confirmedUser = userRepository.save(User.createWithoutId("보예")); - User firstWaitingUser = userRepository.save(User.createWithoutId("수민")); - User secondWaitingUser = userRepository.save(User.createWithoutId("말랑")); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, confirmedUser, ReservationStatus.CONFIRMED, confirmedClock - )); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, firstWaitingUser, ReservationStatus.WAITING, firstWaitingClock - )); - reservationRepository.save(Reservation.createWithoutId( - reservationSlot, secondWaitingUser, ReservationStatus.WAITING, secondWaitingClock - )); - ReservationService reservationService = createReservationService(cancelClock); - - // when - reservationService.cancelUserReservation(firstWaitingUser.getId()); - ReservationWithWaitingNumber confirmedReservation = reservationRepository.findReservations("보예").getFirst(); - ReservationWithWaitingNumber cancelReservation = reservationRepository.findReservations("수민").getFirst(); - ReservationWithWaitingNumber waitingReservation = reservationRepository.findReservations("말랑").getFirst(); - - // then - assertSoftly(softly -> { - assertThat(confirmedReservation.reservation().getStatus()).isEqualTo(ReservationStatus.CONFIRMED); - assertThat(cancelReservation.reservation().getStatus()).isEqualTo(ReservationStatus.CANCELED); - assertThat(waitingReservation.reservation().getStatus()).isEqualTo(ReservationStatus.WAITING); - assertThat(waitingReservation.waitingNumber()).isEqualTo(1); - } - ); - } - @Test @DisplayName("예약 날짜와 시간을 수정한다.") void updateReservationDateAndTime() { // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime beforeReservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationTime afterReservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(15, 0)) - ); - ReservationDate beforeReservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationDate afterReservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 14)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(beforeReservationDate, beforeReservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest( - afterReservationDate.getId(), - afterReservationTime.getId() - ); - - // when - reservationService.updateReservation(savedUserReservation.getId(), request); - - // then - ReservationSlot updatedReservation = reservationRepository.findActiveReservation(savedUserReservation.getId()) - .orElseThrow() - .getReservationSlot(); - assertSoftly(softly -> { - assertThat(updatedReservation.getDate().getDate()).isEqualTo(LocalDate.of(2026, 5, 14)); - assertThat(updatedReservation.getTime().getStartAt()).isEqualTo(LocalTime.of(15, 0)); - assertThat(updatedReservation.getTheme().getName()).isEqualTo("공포"); - }); - } - - @Test - @DisplayName("확정 예약을 같은 날짜와 시간으로 수정하면 수정 시각 기준으로 대기 순서를 재정렬한다.") - void updateReservationToSameSchedule() { - // given - Clock confirmedClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - Clock firstWaitingClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 1)); - Clock secondWaitingClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 2)); - Clock updateClock = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 3)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - User confirmedUser = userRepository.save(User.createWithoutId("보예")); - User firstWaitingUser = userRepository.save(User.createWithoutId("로치")); - User secondWaitingUser = userRepository.save(User.createWithoutId("수민")); - Reservation confirmedReservation = saveConfirmedReservation(savedReservation, confirmedUser, confirmedClock); - Reservation firstWaitingReservation = saveWaitingReservation( - savedReservation, - firstWaitingUser, - firstWaitingClock - ); - Reservation secondWaitingReservation = saveWaitingReservation( - savedReservation, - secondWaitingUser, - secondWaitingClock - ); - ReservationService reservationService = createReservationService(updateClock); - UpdateReservationRequest request = new UpdateReservationRequest( - reservationDate.getId(), - reservationTime.getId() - ); - - // when - reservationService.updateReservation(confirmedReservation.getId(), request); - - // then - Reservation updatedConfirmedReservation = reservationRepository.findActiveReservation( - confirmedReservation.getId()) - .orElseThrow(); - Reservation updatedFirstWaitingReservation = reservationRepository.findActiveReservation( - firstWaitingReservation.getId()) - .orElseThrow(); - Reservation updatedSecondWaitingReservation = reservationRepository.findActiveReservation( - secondWaitingReservation.getId()) - .orElseThrow(); - assertSoftly(softly -> { - assertThat(updatedFirstWaitingReservation.getStatus()).isEqualTo(ReservationStatus.CONFIRMED); - assertThat(updatedSecondWaitingReservation.getStatus()).isEqualTo(ReservationStatus.WAITING); - assertThat(updatedConfirmedReservation.getStatus()).isEqualTo(ReservationStatus.WAITING); - }); - } - - @Test - @DisplayName("예약 시간만 수정한다.") - void updateReservationTimeOnly() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime beforeReservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationTime afterReservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(15, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, beforeReservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest(null, afterReservationTime.getId()); + Long themeId = saveTheme("공포"); + Long beforeDateId = saveDate("2026-06-01"); + Long afterDateId = saveDate("2026-06-02"); + Long beforeTimeId = saveTime("10:00"); + Long afterTimeId = saveTime("15:00"); + Long beforeSlotId = saveReservationSlot(beforeDateId, beforeTimeId, themeId); + Long reservationId = saveReservation("보예", beforeSlotId, ReservationStatus.CONFIRMED, "2026-05-31 13:00:00"); + UpdateReservationRequest request = new UpdateReservationRequest(afterDateId, afterTimeId); // when - reservationService.updateReservation(savedUserReservation.getId(), request); - ReservationSlot updatedReservation = reservationRepository.findActiveReservation(savedUserReservation.getId()) - .orElseThrow() - .getReservationSlot(); + reservationService.updateReservation(reservationId, request); // then + Long updatedSlotId = findReservationSlotId(reservationId); assertSoftly(softly -> { - assertThat(updatedReservation.getDate().getDate()).isEqualTo(LocalDate.of(2026, 5, 13)); - assertThat(updatedReservation.getTime().getStartAt()).isEqualTo(LocalTime.of(15, 0)); + assertThat(findSlotDate(updatedSlotId)).isEqualTo(LocalDate.of(2026, 6, 2)); + assertThat(findSlotTime(updatedSlotId)).isEqualTo(LocalTime.of(15, 0)); + assertThat(findReservationStatus(reservationId)).isEqualTo(ReservationStatus.CONFIRMED); }); } @@ -826,8 +202,6 @@ void updateReservationTimeOnly() { @DisplayName("존재하지 않는 예약을 수정하면 예외가 발생한다.") void throwExceptionWhenUpdatingNonExistentReservation() { // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationService reservationService = createReservationService(now); UpdateReservationRequest request = new UpdateReservationRequest(1L, 2L); // when & then @@ -836,190 +210,94 @@ void throwExceptionWhenUpdatingNonExistentReservation() { .hasMessage("사용자 예약 신청이 존재하지 않습니다."); } - @Test - @DisplayName("존재하지 않는 예약 날짜로 수정하면 예외가 발생한다.") - void throwExceptionWhenUpdatingReservationWithNonExistentDate() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest(999L, null); - - // when & then - assertThatThrownBy(() -> reservationService.updateReservation(savedUserReservation.getId(), request)) - .isInstanceOf(RoomescapeException.class) - .hasMessage("존재하지 않는 날짜 입니다."); + private Long saveTheme(String name) { + Theme theme = themeRepository.save(Theme.createWithoutId(name, "무서운 테마", "theme-url")); + return theme.getId(); } - @Test - @DisplayName("존재하지 않는 예약 시간으로 수정하면 예외가 발생한다.") - void throwExceptionWhenUpdatingReservationWithNonExistentTime() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); + private Long saveDate(String date) { ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ReservationDate.createWithoutId(LocalDate.parse(date)) ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest(null, 999L); - - // when & then - assertThatThrownBy(() -> reservationService.updateReservation(savedUserReservation.getId(), request)) - .isInstanceOf(RoomescapeException.class) - .hasMessage("존재하지 않는 예약 시간대 입니다."); + return reservationDate.getId(); } - @Test - @DisplayName("오늘보다 이전 날짜로 예약을 수정할 수 없다.") - void throwExceptionWhenUpdatingReservationToDateBeforeToday() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + private Long saveTime(String time) { ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(15, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - ReservationDate beforeToday = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 11)) + ReservationTime.createWithoutId(LocalTime.parse(time)) ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest(beforeToday.getId(), null); - - // when & then - assertThatThrownBy(() -> reservationService.updateReservation(savedUserReservation.getId(), request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); + return reservationTime.getId(); } - @Test - @DisplayName("오늘 예약을 현재 시간보다 이전으로 수정할 수 없다.") - void throwExceptionWhenUpdatingReservationToTimeBeforeNowOnToday() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(15, 0)) - ); - ReservationTime beforeNow = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(12, 59)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( + private Long saveReservationSlot(Long dateId, Long timeId, Long themeId) { + ReservationDate reservationDate = reservationDateRepository.findById(dateId).orElseThrow(); + ReservationTime reservationTime = reservationTimeRepository.findById(timeId).orElseThrow(); + Theme theme = themeRepository.findById(themeId).orElseThrow(); + ReservationSlot reservationSlot = reservationSlotRepository.save( ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) ); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest(null, beforeNow.getId()); - - // when & then - assertThatThrownBy(() -> reservationService.updateReservation(savedUserReservation.getId(), request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("예약 시간은 현재 이후여야 합니다. 현재 시각:" + LocalTime.of(13, 0)); + return reservationSlot.getId(); } - @Test - @DisplayName("중복된 예약으로 수정하면 예외가 발생한다.") - void throwExceptionWhenUpdatingReservationToDuplicatedSchedule() { - // given - Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationTime otherReservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(15, 0)) - ); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) - ); - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlot savedReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, reservationTime, theme) - ); - ReservationSlot otherReservation = reservationSlotRepository.save( - ReservationSlot.createWithoutId(reservationDate, otherReservationTime, theme) + private Long saveReservation( + String username, + Long reservationSlotId, + ReservationStatus status, + String updatedAt + ) { + Long userId = saveUser(username); + User user = userRepository.findById(userId).orElseThrow(); + ReservationSlot reservationSlot = reservationSlotRepository.findById(reservationSlotId).orElseThrow(); + Reservation reservation = reservationRepository.save( + Reservation.createWithoutId( + reservationSlot, + user, + status, + fixedClockAt(updatedAt) + ) ); - User user = userRepository.save(User.createWithoutId("보예")); - Reservation savedUserReservation = saveConfirmedReservation(savedReservation, user, now); - saveConfirmedReservation(otherReservation, user, now); - ReservationService reservationService = createReservationService(now); - UpdateReservationRequest request = new UpdateReservationRequest(null, otherReservationTime.getId()); + return reservation.getId(); + } - // when & then - assertThatThrownBy(() -> reservationService.updateReservation(savedUserReservation.getId(), request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("중복 예약입니다. 예약 정보를 다시 확인해주세요."); + private Long saveUser(String username) { + User user = userRepository.save(User.createWithoutId(username)); + return user.getId(); } - private Reservation saveConfirmedReservation(ReservationSlot reservationSlot, Clock clock) { - User user = userRepository.save(User.createWithoutId("보예")); - return saveConfirmedReservation(reservationSlot, user, clock); + private ReservationStatus findReservationStatus(Long reservationId) { + return reservationRepository.findById(reservationId) + .orElseThrow() + .getStatus(); } - private ReservationService createReservationService(Clock clock) { - return new ReservationService( - reservationRepository, - new UserService(userRepository), - new ReservationSlotService( - reservationSlotRepository, - themeRepository, - reservationDateRepository, - reservationRepository - ), - new ThemeService(themeRepository, reservationSlotRepository, clock), - new ReservationDateService(reservationSlotRepository, reservationDateRepository), - new ReservationTimeService(reservationTimeRepository, reservationSlotRepository), - clock - ); + private Long findReservationSlotId(Long reservationId) { + return reservationRepository.findById(reservationId) + .orElseThrow() + .getReservationSlot() + .getId(); } - private Reservation saveConfirmedReservation(ReservationSlot reservationSlot, User user, Clock clock) { - return reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - user, - ReservationStatus.CONFIRMED, - clock - )); + private LocalDate findSlotDate(Long reservationSlotId) { + return reservationSlotRepository.findById(reservationSlotId) + .orElseThrow() + .getDate() + .getDate(); } - private Reservation saveWaitingReservation( - ReservationSlot reservationSlot, - User user, - Clock clock - ) { - return reservationRepository.save(Reservation.createWithoutId( - reservationSlot, - user, - ReservationStatus.WAITING, - clock - )); + private LocalTime findSlotTime(Long reservationSlotId) { + return reservationSlotRepository.findById(reservationSlotId) + .orElseThrow() + .getTime() + .getStartAt(); } - private Clock fixedClockAt(LocalDateTime dateTime) { - return Clock.fixed(dateTime.atZone(ZONE_ID).toInstant(), ZONE_ID); + private Clock fixedClockAt(String dateTime) { + ZoneId zoneId = ZoneId.systemDefault(); + return Clock.fixed( + LocalDateTime.parse(dateTime.replace(" ", "T")) + .atZone(zoneId) + .toInstant(), + zoneId + ); } } diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java index e0192d3ec2..527d5a400c 100644 --- a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java @@ -3,54 +3,54 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; import java.time.LocalDate; -import java.time.LocalTime; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import roomescape.domain.reservationslot.ReservationSlot; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import roomescape.domain.reservationdate.admin.dto.AdminReservationDateResponse; import roomescape.domain.reservationdate.admin.dto.CreateReservationDateRequest; import roomescape.domain.reservationdate.admin.dto.CreateReservationDateResponse; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.theme.Theme; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.support.exception.RoomescapeException; -import roomescape.support.fake.FakeReservationDateRepository; -import roomescape.support.fake.FakeReservationSlotRepository; +@ExtendWith(MockitoExtension.class) class ReservationDateServiceTest { - private FakeReservationSlotRepository reservationRepository; - private FakeReservationDateRepository reservationDateRepository; + @Mock + private JpaReservationSlotRepository reservationSlotRepository; - @BeforeEach - void setUp() { - reservationRepository = new FakeReservationSlotRepository(); - reservationDateRepository = new FakeReservationDateRepository(); - } + @Mock + private JpaReservationDateRepository reservationDateRepository; + + @InjectMocks + private ReservationDateService reservationDateService; @Test @DisplayName("예약 날짜를 생성한다.") void createReservationDate() { // given - ReservationDateService reservationDateService = new ReservationDateService( - reservationRepository, - reservationDateRepository - ); + ReservationDate savedReservationDate = ReservationDate.of(1L, LocalDate.of(2026, 5, 4)); + given(reservationDateRepository.save(any(ReservationDate.class))) + .willReturn(savedReservationDate); // when CreateReservationDateResponse response = reservationDateService.createReservationDate( new CreateReservationDateRequest(LocalDate.of(2026, 5, 4)) ); - ReservationDate reservationDate = reservationDateRepository.findById(response.id()).orElseThrow(); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(reservationDate.getId()); + assertThat(response.id()).isEqualTo(savedReservationDate.getId()); assertThat(response.reservationDate()).isEqualTo(LocalDate.of(2026, 5, 4)); - assertThat(reservationDate.getDate()).isEqualTo(LocalDate.of(2026, 5, 4)); }); } @@ -58,12 +58,8 @@ void createReservationDate() { @DisplayName("예약 날짜 목록을 조회한다.") void getReservationDateList() { // given - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); - ReservationDateService reservationDateService = new ReservationDateService( - reservationRepository, - reservationDateRepository - ); + ReservationDate reservationDate = ReservationDate.of(1L, LocalDate.of(2026, 5, 4)); + given(reservationDateRepository.findAll()).willReturn(List.of(reservationDate)); // when List responses = reservationDateService.getAllReservationDateForAdmin(); @@ -80,40 +76,27 @@ void getReservationDateList() { @DisplayName("이미 예약이 존재하는 날짜는 삭제할 수 없다.") void throwExceptionWhenDeletingDateInUse() { // given - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); - reservationRepository.save( - ReservationSlot.createWithoutId(reservationDate, - ReservationTime.of(1L, LocalTime.of(10, 0)), - Theme.of(1L, "공포", "무서운 테마", "theme-url") - ) - ); - ReservationDateService reservationDateService = new ReservationDateService( - reservationRepository, - reservationDateRepository - ); + Long reservationDateId = 1L; + given(reservationSlotRepository.countByDateId(reservationDateId)).willReturn(1); // when & then - assertThatThrownBy(() -> reservationDateService.deleteReservationDate(reservationDate.getId())) + assertThatThrownBy(() -> reservationDateService.deleteReservationDate(reservationDateId)) .isInstanceOf(RoomescapeException.class) .hasMessage("이미 예약이 존재하는 날짜는 삭제할 수 없습니다."); + verify(reservationDateRepository, never()).deleteById(reservationDateId); } @Test @DisplayName("예약이 없는 날짜는 삭제한다.") void deleteDateWhenNoReservationExists() { // given - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); - ReservationDateService reservationDateService = new ReservationDateService( - reservationRepository, - reservationDateRepository - ); + Long reservationDateId = 1L; + given(reservationSlotRepository.countByDateId(reservationDateId)).willReturn(0); // when - reservationDateService.deleteReservationDate(reservationDate.getId()); + reservationDateService.deleteReservationDate(reservationDateId); // then - assertThat(reservationDateRepository.findById(reservationDate.getId())).isEmpty(); + verify(reservationDateRepository).deleteById(reservationDateId); } } diff --git a/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java b/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java index 356c5ce2f2..af44baeeab 100644 --- a/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java +++ b/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java @@ -5,57 +5,59 @@ import static org.assertj.core.api.BDDAssertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.time.LocalDate; import java.time.LocalTime; import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.dao.DuplicateKeyException; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.dto.ReservationCountResult; +import roomescape.domain.reservationdate.JpaReservationDateRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationslot.dto.ReservationSlotResponse; import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.JpaThemeRepository; import roomescape.domain.theme.Theme; import roomescape.support.exception.NotFoundException; -import roomescape.support.fake.FakeReservationDateRepository; -import roomescape.support.fake.FakeReservationSlotRepository; -import roomescape.support.fake.FakeThemeRepository; +@ExtendWith(MockitoExtension.class) class ReservationSlotServiceTest { - private FakeReservationSlotRepository reservationSlotRepository; - private FakeThemeRepository themeRepository; - private FakeReservationDateRepository reservationDateRepository; + @Mock + private JpaReservationSlotRepository reservationSlotRepository; + + @Mock + private JpaThemeRepository themeRepository; + + @Mock + private JpaReservationDateRepository reservationDateRepository; + + @Mock private ReservationRepository reservationRepository; - @BeforeEach - void setUp() { - reservationSlotRepository = new FakeReservationSlotRepository(); - themeRepository = new FakeThemeRepository(); - reservationDateRepository = new FakeReservationDateRepository(); - reservationRepository = mock(ReservationRepository.class); - } + @InjectMocks + private ReservationSlotService reservationSlotService; @Test @DisplayName("예약 슬롯별 현재 예약 인원을 조회한다.") void getReservationSlots() { // given - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 16)) - ); + Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); + ReservationDate reservationDate = ReservationDate.of(1L, LocalDate.of(2026, 5, 16)); + given(themeRepository.findById(theme.getId())).willReturn(Optional.of(theme)); + given(reservationDateRepository.findById(reservationDate.getId())).willReturn(Optional.of(reservationDate)); given(reservationRepository.countReservation(theme.getId(), reservationDate.getId())) .willReturn(List.of( ReservationCountResult.of(1L, LocalTime.of(10, 0), 2L), ReservationCountResult.of(2L, LocalTime.of(11, 0), 0L) )); - ReservationSlotService reservationSlotService = createReservationSlotService(); // when List responses = reservationSlotService.getReservationSlots( @@ -80,13 +82,12 @@ void getReservationSlots() { @DisplayName("존재하지 않는 테마로 예약 슬롯을 조회할 수 없다.") void throwExceptionWhenThemeDoesNotExist() { // given - ReservationDate reservationDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.of(2026, 5, 16)) - ); - ReservationSlotService reservationSlotService = createReservationSlotService(); + Long themeId = 1L; + Long reservationDateId = 1L; + given(themeRepository.findById(themeId)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> reservationSlotService.getReservationSlots(1L, reservationDate.getId())) + assertThatThrownBy(() -> reservationSlotService.getReservationSlots(themeId, reservationDateId)) .isInstanceOf(NotFoundException.class) .hasMessage("존재하지 않는 테마 입니다."); } @@ -95,11 +96,13 @@ void throwExceptionWhenThemeDoesNotExist() { @DisplayName("존재하지 않는 날짜로 예약 슬롯을 조회할 수 없다.") void throwExceptionWhenDateDoesNotExist() { // given - Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); - ReservationSlotService reservationSlotService = createReservationSlotService(); + Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); + Long reservationDateId = 1L; + given(themeRepository.findById(theme.getId())).willReturn(Optional.of(theme)); + given(reservationDateRepository.findById(reservationDateId)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> reservationSlotService.getReservationSlots(theme.getId(), 1L)) + assertThatThrownBy(() -> reservationSlotService.getReservationSlots(theme.getId(), reservationDateId)) .isInstanceOf(NotFoundException.class) .hasMessage("존재하지 않는 날짜 입니다."); } @@ -108,27 +111,15 @@ void throwExceptionWhenDateDoesNotExist() { @DisplayName("unique 조건을 위반할 경우 재조회를 한다.") void findExistingSlotWhenDuplicateKeyOccurs() { // given - ReservationSlotRepository reservationSlotRepository = mock(ReservationSlotRepository.class); ReservationDate date = ReservationDate.of(1L, LocalDate.of(2026, 6, 1)); ReservationTime time = ReservationTime.of(1L, LocalTime.of(10, 0)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); ReservationSlot existingSlot = ReservationSlot.of(1L, date, time, theme); - when( - reservationSlotRepository.findByScheduleToUpdate(time.getId(), date.getId(), theme.getId()) - ) - .thenReturn(Optional.empty()) - .thenReturn(Optional.of(existingSlot)); - when( - reservationSlotRepository.save(any()) - ).thenThrow( - new DuplicateKeyException("unique 제약조건을 위반했습니다.") - ); - ReservationSlotService reservationSlotService = new ReservationSlotService( - reservationSlotRepository, - themeRepository, - reservationDateRepository, - reservationRepository - ); + given(reservationSlotRepository.findByScheduleToUpdate(time.getId(), date.getId(), theme.getId())) + .willReturn(Optional.empty()) + .willReturn(Optional.of(existingSlot)); + given(reservationSlotRepository.save(any(ReservationSlot.class))) + .willThrow(new DataIntegrityViolationException("unique 제약조건을 위반했습니다.")); // when ReservationSlot reservationSlot = reservationSlotService.findOrCreateReservationSlot(date, time, theme); @@ -136,13 +127,4 @@ void findExistingSlotWhenDuplicateKeyOccurs() { // then assertThat(reservationSlot).isEqualTo(existingSlot); } - - private ReservationSlotService createReservationSlotService() { - return new ReservationSlotService( - reservationSlotRepository, - themeRepository, - reservationDateRepository, - reservationRepository - ); - } } diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index 2e2c0cff7c..121f643281 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -4,51 +4,54 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.BDDAssertions.tuple; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; -import java.time.LocalDate; import java.time.LocalTime; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationslot.ReservationSlot; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.reservationtime.admin.dto.CreateTimeRequest; import roomescape.domain.reservationtime.admin.dto.CreateTimeResponse; import roomescape.domain.reservationtime.admin.dto.ReservationTimeResponse; -import roomescape.domain.theme.Theme; import roomescape.support.exception.RoomescapeException; -import roomescape.support.fake.FakeReservationSlotRepository; -import roomescape.support.fake.FakeReservationTimeRepository; +@ExtendWith(MockitoExtension.class) class ReservationTimeServiceTest { - private FakeReservationSlotRepository reservationRepository; - private FakeReservationTimeRepository reservationTimeRepository; + @Mock + private JpaReservationTimeRepository reservationTimeRepository; - @BeforeEach - void setUp() { - reservationRepository = new FakeReservationSlotRepository(); - reservationTimeRepository = new FakeReservationTimeRepository(); - } + @Mock + private JpaReservationSlotRepository reservationSlotRepository; + + @InjectMocks + private ReservationTimeService reservationTimeService; @Test @DisplayName("예약 시간을 생성한다.") void createReservationTime() { // given - ReservationTimeService reservationTimeService = createReservationTimeService(); + ReservationTime savedReservationTime = ReservationTime.of(1L, LocalTime.of(10, 0)); + given(reservationTimeRepository.save(any(ReservationTime.class))) + .willReturn(savedReservationTime); // when CreateTimeResponse response = reservationTimeService.createReservationTime( new CreateTimeRequest(LocalTime.of(10, 0)) ); - ReservationTime savedReservationTime = reservationTimeRepository.findById(response.id()).orElseThrow(); // then assertSoftly(softly -> { assertThat(response.id()).isEqualTo(savedReservationTime.getId()); assertThat(response.startAt()).isEqualTo(LocalTime.of(10, 0)); - assertThat(savedReservationTime.getStartAt()).isEqualTo(LocalTime.of(10, 0)); }); } @@ -56,9 +59,10 @@ void createReservationTime() { @DisplayName("예약 시간 목록을 조회한다.") void getReservationTimeList() { // given - reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(10, 0))); - reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(11, 0))); - ReservationTimeService reservationTimeService = createReservationTimeService(); + given(reservationTimeRepository.findAll()).willReturn(List.of( + ReservationTime.of(1L, LocalTime.of(10, 0)), + ReservationTime.of(2L, LocalTime.of(11, 0)) + )); // when List responses = reservationTimeService.getAllReservationTime(); @@ -79,43 +83,27 @@ void getReservationTimeList() { @DisplayName("이미 예약이 존재하는 시간은 삭제할 수 없다.") void throwExceptionWhenDeletingTimeInUse() { // given - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - reservationRepository.save( - ReservationSlot.createWithoutId(ReservationDate.of(1L, LocalDate.of(2026, 5, 12)), - reservationTime, - Theme.of(1L, "공포", "무서운 테마", "theme-url") - ) - ); - ReservationTimeService reservationTimeService = createReservationTimeService(); + Long reservationTimeId = 1L; + given(reservationSlotRepository.countByTimeId(reservationTimeId)).willReturn(1); // when & then - assertThatThrownBy(() -> reservationTimeService.deleteReservationTime(reservationTime.getId())) + assertThatThrownBy(() -> reservationTimeService.deleteReservationTime(reservationTimeId)) .isInstanceOf(RoomescapeException.class) .hasMessage("이미 예약이 존재하는 시간대는 삭제할 수 없습니다."); + verify(reservationTimeRepository, never()).deleteById(reservationTimeId); } @Test @DisplayName("예약이 없는 시간은 삭제한다.") void deleteTimeWhenNoReservationExists() { // given - ReservationTime reservationTime = reservationTimeRepository.save( - ReservationTime.createWithoutId(LocalTime.of(10, 0)) - ); - ReservationTimeService reservationTimeService = createReservationTimeService(); + Long reservationTimeId = 1L; + given(reservationSlotRepository.countByTimeId(reservationTimeId)).willReturn(0); // when - reservationTimeService.deleteReservationTime(reservationTime.getId()); + reservationTimeService.deleteReservationTime(reservationTimeId); // then - assertThat(reservationTimeRepository.findById(reservationTime.getId())).isEmpty(); - } - - private ReservationTimeService createReservationTimeService() { - return new ReservationTimeService( - reservationTimeRepository, - reservationRepository - ); + verify(reservationTimeRepository).deleteById(reservationTimeId); } } diff --git a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java index e04a4b5824..3c180e95fb 100644 --- a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java @@ -2,38 +2,53 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.theme.admin.dto.AdminThemeResponse; import roomescape.domain.theme.admin.dto.CreateThemeRequest; import roomescape.domain.theme.admin.dto.CreateThemeResponse; import roomescape.domain.theme.dto.ThemeResponse; -import roomescape.support.fake.FakeReservationSlotRepository; -import roomescape.support.fake.FakeThemeRepository; +@ExtendWith(MockitoExtension.class) class ThemeServiceTest { - private FakeReservationSlotRepository reservationRepository; - private FakeThemeRepository themeRepository; + @Mock + private JpaThemeRepository themeRepository; - @BeforeEach - void setUp() { - reservationRepository = new FakeReservationSlotRepository(); - themeRepository = new FakeThemeRepository(); - } + @Mock + private JpaReservationSlotRepository reservationSlotRepository; + + @Mock + private ThemeRepository themeQueryRepository; + + private final Clock clock = Clock.fixed( + Instant.parse("2026-05-27T00:00:00Z"), + ZoneId.of("Asia/Seoul") + ); + + @InjectMocks + private ThemeService themeService; @Test @DisplayName("관리자용 테마 목록을 조회한다.") void getThemeListForAdmin() { // given - themeRepository.save(Theme.createWithoutId("미스터리", "보예의 미스터리", "theme-url")); - ThemeService themeService = new ThemeService(themeRepository, reservationRepository, fixedClock()); + given(themeRepository.findAll()).willReturn(List.of( + Theme.of(1L, "미스터리", "보예의 미스터리", "theme-url") + )); // when List responses = themeService.getAllThemeForAdmin(); @@ -52,8 +67,9 @@ void getThemeListForAdmin() { @DisplayName("사용자용 테마 목록을 조회한다.") void getThemeListForUser() { // given - themeRepository.save(Theme.createWithoutId("미스터리", "보예의 미스터리", "theme-url")); - ThemeService themeService = new ThemeService(themeRepository, reservationRepository, fixedClock()); + given(themeRepository.findAll()).willReturn(List.of( + Theme.of(1L, "미스터리", "보예의 미스터리", "theme-url") + )); // when List responses = themeService.getAllTheme(); @@ -72,23 +88,20 @@ void getThemeListForUser() { @DisplayName("테마를 생성한다.") void createTheme() { // given - ThemeService themeService = new ThemeService(themeRepository, reservationRepository, fixedClock()); + Theme savedTheme = Theme.of(1L, "미스터리", "보예의 미스터리", "theme-url"); + given(themeRepository.save(any(Theme.class))).willReturn(savedTheme); // when CreateThemeResponse response = themeService.createTheme( new CreateThemeRequest("미스터리", "보예의 미스터리", "theme-url") ); - Theme theme = themeRepository.findById(response.id()).orElseThrow(); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(theme.getId()); + assertThat(response.id()).isEqualTo(savedTheme.getId()); assertThat(response.name()).isEqualTo("미스터리"); assertThat(response.content()).isEqualTo("보예의 미스터리"); assertThat(response.url()).isEqualTo("theme-url"); - assertThat(theme.getName()).isEqualTo("미스터리"); - assertThat(theme.getContent()).isEqualTo("보예의 미스터리"); - assertThat(theme.getUrl()).isEqualTo("theme-url"); }); } @@ -96,22 +109,13 @@ void createTheme() { @DisplayName("테마를 삭제한다.") void deleteTheme() { // given - Theme theme = themeRepository.save( - Theme.createWithoutId("공포", "무섭다", "theme-url") - ); - ThemeService themeService = new ThemeService(themeRepository, reservationRepository, fixedClock()); + Long themeId = 1L; + given(reservationSlotRepository.countByThemeId(themeId)).willReturn(0); // when - themeService.deleteTheme(theme.getId()); + themeService.deleteTheme(themeId); // then - assertThat(themeRepository.findById(theme.getId())).isEmpty(); - } - - private Clock fixedClock() { - return Clock.fixed( - Instant.parse("2026-05-27T00:00:00Z"), - ZoneId.of("Asia/Seoul") - ); + verify(themeRepository).deleteById(themeId); } } diff --git a/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java b/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java deleted file mode 100644 index 3fb44cd073..0000000000 --- a/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package roomescape.support.fake; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationdate.ReservationDateRepository; - -public class FakeReservationDateRepository implements ReservationDateRepository { - - private final Map storage = new LinkedHashMap<>(); - private long sequence = 1L; - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(storage.get(id)); - } - - @Override - public List findAll() { - return new ArrayList<>(storage.values()); - } - - @Override - public ReservationDate save(ReservationDate reservationDate) { - Long id = reservationDate.getId(); - if (id == null) { - id = sequence++; - } else { - sequence = Math.max(sequence, id + 1); - } - ReservationDate savedReservationDate = ReservationDate.of(id, reservationDate.getDate()); - storage.put(id, savedReservationDate); - return savedReservationDate; - } - - @Override - public int deleteById(Long id) { - ReservationDate removedReservationDate = storage.remove(id); - if (removedReservationDate == null) { - return 0; - } - return 1; - } - - @Override - public Optional findByDate(LocalDate startWhen) { - return storage.values().stream() - .filter(reservationDate -> startWhen.equals(reservationDate.getDate())) - .findFirst(); - } -} diff --git a/src/test/java/roomescape/support/fake/FakeReservationRepository.java b/src/test/java/roomescape/support/fake/FakeReservationRepository.java deleted file mode 100644 index 42c019b12f..0000000000 --- a/src/test/java/roomescape/support/fake/FakeReservationRepository.java +++ /dev/null @@ -1,146 +0,0 @@ -package roomescape.support.fake; - -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.reservation.ReservationStatus; -import roomescape.domain.reservation.dto.ReservationCountResult; -import roomescape.domain.reservation.dto.ReservationWithWaitingNumber; - -public class FakeReservationRepository implements ReservationRepository { - - private final Map storage = new LinkedHashMap<>(); - private long sequence = 1L; - - @Override - public Reservation save(Reservation userReservation) { - Long id = userReservation.getId(); - if (id == null) { - id = sequence++; - } else { - sequence = Math.max(sequence, id + 1); - } - Reservation savedUserReservation = Reservation.createWithId(id, userReservation); - storage.put(id, savedUserReservation); - return savedUserReservation; - } - - @Override - public List findAll() { - return storage.values().stream() - .map(this::withWaitingNumber) - .toList(); - } - - @Override - public Optional findActiveReservation(Long id) { - return Optional.ofNullable(storage.get(id)) - .filter(this::isActive); - } - - @Override - public List findReservations(String username) { - return storage.values().stream() - .filter(userReservation -> username.equals(userReservation.getUser().getName())) - .sorted(Comparator.comparing(Reservation::getId).reversed()) - .map(this::withWaitingNumber) - .toList(); - } - - @Override - public Long countByReservationSlotId(Long reservationSlotId) { - return storage.values().stream() - .filter(this::isActive) - .filter(userReservation -> reservationSlotId.equals(userReservation.getReservationSlot().getId())) - .count(); - } - - @Override - public List findReservationsInWaitingOrder(Long reservationSlotId) { - return storage.values().stream() - .filter(this::isActive) - .filter(userReservation -> reservationSlotId.equals(userReservation.getReservationSlot().getId())) - .sorted(Comparator.comparing(Reservation::getUpdatedAt) - .thenComparing(Reservation::getId)) - .toList(); - } - - @Override - public void update(Long id, Reservation userReservation) { - if (!storage.containsKey(id)) { - return; - } - Reservation updatedUserReservation = Reservation.createWithId(id, userReservation); - storage.put(id, updatedUserReservation); - } - - @Override - public boolean existsActiveByUserIdAndReservationId(Long userId, Long reservationId) { - return storage.values().stream() - .filter(this::isActive) - .filter(userReservation -> userId.equals(userReservation.getUser().getId())) - .filter(userReservation -> reservationId.equals(userReservation.getReservationSlot().getId())) - .findAny() - .isPresent(); - } - - @Override - public List countReservation(Long themeId, Long dateId) { - return storage.values().stream() - .filter(this::isActive) - .filter(reservation -> themeId.equals(reservation.getReservationSlot().getTheme().getId())) - .filter(reservation -> dateId.equals(reservation.getReservationSlot().getDate().getId())) - .collect(java.util.stream.Collectors.groupingBy( - reservation -> reservation.getReservationSlot().getTime().getId(), - LinkedHashMap::new, - java.util.stream.Collectors.toList() - )) - .values() - .stream() - .map(reservations -> { - Reservation firstReservation = reservations.getFirst(); - return ReservationCountResult.of( - firstReservation.getReservationSlot().getTime().getId(), - firstReservation.getReservationSlot().getTime().getStartAt(), - reservations.size() - ); - }) - .sorted(Comparator.comparing(ReservationCountResult::startAt)) - .toList(); - } - - private boolean isActive(Reservation reservation) { - return reservation.getStatus() != ReservationStatus.CANCELED; - } - - private ReservationWithWaitingNumber withWaitingNumber(Reservation reservation) { - return new ReservationWithWaitingNumber( - reservation, - waitingNumberOf(reservation) - ); - } - - private Long waitingNumberOf(Reservation reservation) { - if (reservation.getStatus() != ReservationStatus.WAITING) { - return null; - } - List orderedReservations = storage.values().stream() - .filter(this::isActive) - .filter(storedReservation -> storedReservation.getStatus() == ReservationStatus.WAITING) - .filter(storedReservation -> reservation.getReservationSlot().getId() - .equals(storedReservation.getReservationSlot().getId())) - .sorted(Comparator.comparing(Reservation::getUpdatedAt) - .thenComparing(Reservation::getId)) - .toList(); - - int order = orderedReservations.indexOf(reservation); - if (order < 0) { - return null; - } - return (long) order + 1; - } -} diff --git a/src/test/java/roomescape/support/fake/FakeReservationSlotRepository.java b/src/test/java/roomescape/support/fake/FakeReservationSlotRepository.java deleted file mode 100644 index c7f4b3912b..0000000000 --- a/src/test/java/roomescape/support/fake/FakeReservationSlotRepository.java +++ /dev/null @@ -1,103 +0,0 @@ -package roomescape.support.fake; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import roomescape.domain.reservationslot.ReservationSlot; -import roomescape.domain.reservationslot.ReservationSlotRepository; - -public class FakeReservationSlotRepository implements ReservationSlotRepository { - - private final Map storage = new LinkedHashMap<>(); - private long sequence = 1L; - - public Optional findById(Long id) { - return Optional.ofNullable(storage.get(id)); - } - - @Override - public ReservationSlot save(ReservationSlot reservation) { - Long id = reservation.getId(); - if (id == null) { - id = sequence++; - } else { - sequence = Math.max(sequence, id + 1); - } - ReservationSlot savedReservation = ReservationSlot.createWithId(id, reservation); - storage.put(id, savedReservation); - return savedReservation; - } - - @Override - public List findAll() { - return new ArrayList<>(storage.values()); - } - - @Override - public int deleteById(Long id) { - ReservationSlot removedReservation = storage.remove(id); - if (removedReservation == null) { - return 0; - } - return 1; - } - - @Override - public int countByTimeId(Long timeId) { - int count = 0; - for (ReservationSlot value : storage.values()) { - if (value.getTime().getId().equals(timeId)) { - count++; - } - } - return count; - } - - @Override - public int countByReservationDateId(Long dateId) { - int count = 0; - for (ReservationSlot value : storage.values()) { - if (value.getDate().getId().equals(dateId)) { - count++; - } - } - return count; - } - - @Override - public int countByThemeId(Long themeId) { - int count = 0; - for (ReservationSlot reservation : storage.values()) { - if (reservation.getTheme().getId().equals(themeId)) { - count++; - } - } - return count; - } - - @Override - public Optional findBySchedule(Long timeId, Long dateId, Long themeId) { - return storage.values().stream() - .filter(reservation -> timeId.equals(reservation.getTime().getId())) - .filter(reservation -> dateId.equals(reservation.getDate().getId())) - .filter(reservation -> themeId.equals(reservation.getTheme().getId())) - .findFirst(); - } - - @Override - public Optional findByScheduleToUpdate(Long timeId, Long dateId, Long themeId) { - return findBySchedule(timeId, dateId, themeId); - } - - @Override - public Optional update(Long id, ReservationSlot withoutId) { - if (!storage.containsKey(id)) { - return Optional.empty(); - } - ReservationSlot updatedReservation = ReservationSlot.createWithId(id, withoutId); - storage.put(id, updatedReservation); - return Optional.of(updatedReservation); - } -} diff --git a/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java b/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java deleted file mode 100644 index 8ab8759081..0000000000 --- a/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package roomescape.support.fake; - -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.reservationtime.ReservationTimeRepository; - -public class FakeReservationTimeRepository implements ReservationTimeRepository { - - private final Map storage = new LinkedHashMap<>(); - private long sequence = 1L; - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(storage.get(id)); - } - - @Override - public ReservationTime save(ReservationTime reservationTime) { - Long id = reservationTime.getId(); - if (id == null) { - id = sequence++; - } else { - sequence = Math.max(sequence, id + 1); - } - ReservationTime savedReservationTime = ReservationTime.of(id, reservationTime.getStartAt()); - storage.put(id, savedReservationTime); - return savedReservationTime; - } - - @Override - public List findAll() { - return new ArrayList<>(storage.values()); - } - - @Override - public int deleteById(Long id) { - ReservationTime removedReservationTime = storage.remove(id); - if (removedReservationTime == null) { - return 0; - } - return 1; - } - - @Override - public Optional findByStartAt(LocalTime startAt) { - return storage.values().stream() - .filter(reservationTime -> startAt.equals(reservationTime.getStartAt())) - .findFirst(); - } -} diff --git a/src/test/java/roomescape/support/fake/FakeThemeRepository.java b/src/test/java/roomescape/support/fake/FakeThemeRepository.java deleted file mode 100644 index 0695ead433..0000000000 --- a/src/test/java/roomescape/support/fake/FakeThemeRepository.java +++ /dev/null @@ -1,54 +0,0 @@ -package roomescape.support.fake; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeRepository; -import roomescape.domain.theme.dto.ThemeRankResult; - -public class FakeThemeRepository implements ThemeRepository { - - private final Map storage = new LinkedHashMap<>(); - private long sequence = 1L; - - @Override - public List findAll() { - return new ArrayList<>(storage.values()); - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(storage.get(id)); - } - - @Override - public List findPopularThemes(int rankLimit, LocalDate startDay, LocalDate today) { - return List.of(); - } - - @Override - public Theme save(Theme theme) { - Long id = theme.getId(); - if (id == null) { - id = sequence++; - } else { - sequence = Math.max(sequence, id + 1); - } - Theme savedTheme = Theme.of(id, theme.getName(), theme.getContent(), theme.getUrl()); - storage.put(id, savedTheme); - return savedTheme; - } - - @Override - public int deleteById(Long id) { - Theme removedTheme = storage.remove(id); - if (removedTheme == null) { - return 0; - } - return 1; - } -} diff --git a/src/test/java/roomescape/support/fake/FakeUserRepository.java b/src/test/java/roomescape/support/fake/FakeUserRepository.java deleted file mode 100644 index 1d4eae881b..0000000000 --- a/src/test/java/roomescape/support/fake/FakeUserRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package roomescape.support.fake; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import roomescape.domain.user.User; -import roomescape.domain.user.UserRepository; - -public class FakeUserRepository implements UserRepository { - - private final Map storage = new LinkedHashMap<>(); - private long sequence = 1L; - - @Override - public User save(User user) { - Long id = user.getId(); - if (id == null) { - id = sequence++; - } else { - sequence = Math.max(sequence, id + 1); - } - User savedUser = User.createWithId(id, user); - storage.put(id, savedUser); - return savedUser; - } - - @Override - public List findAll() { - return new ArrayList<>(storage.values()); - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(storage.get(id)); - } - - @Override - public Optional findByName(String name) { - return storage.values().stream() - .filter(user -> name.equals(user.getName())) - .findFirst(); - } -} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index ea24942fa4..4b628fac5d 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,5 +1,11 @@ spring: datasource: url: jdbc:h2:mem:${random.uuid};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + sql: + init: + mode: never + jpa: + hibernate: + ddl-auto: create-drop token: "" From 87103b6fbe50a16f265a1244d7f1f29069ad2025 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 18 Jun 2026 10:49:30 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationIntegrationTest.java | 14 +++++++------- .../ReservationSlotIntegrationTest.java | 3 ++- .../admin/AdminReservationSlotIntegrationTest.java | 3 ++- .../domain/theme/ThemeIntegrationTest.java | 3 ++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java index bb10df250b..1372fd60ae 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java @@ -7,7 +7,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doAnswer; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -38,7 +37,7 @@ class ReservationIntegrationTest { private JdbcTemplate jdbcTemplate; @MockitoSpyBean - private ReservationRepository reservationRepository; + private JpaReservationRepository reservationRepository; @TestConfiguration static class TestClockConfig { @@ -200,14 +199,14 @@ void checkRollback() { reservationSlotId, ReservationStatus.WAITING ); - doAnswer(invocationOnMock -> - { - if (invocationOnMock.getArgument(0).equals(waitingReservationId)) { + doAnswer(invocationOnMock -> { + Reservation reservation = invocationOnMock.getArgument(0); + if (reservation.getId().equals(waitingReservationId)) { throw new IllegalArgumentException("대기 전환에 실패했습니다."); } return invocationOnMock.callRealMethod(); } - ).when(reservationRepository).update(anyLong(), any(Reservation.class)); + ).when(reservationRepository).save(any(Reservation.class)); // when & then given(). @@ -271,7 +270,8 @@ private Long saveReservation(String name, Long reservationSlotId, ReservationSta name ); jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status) VALUES (?, ?, ?)", + "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " + + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", userId, reservationSlotId, status.name() diff --git a/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java b/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java index 918257b83b..cd3ff4081c 100644 --- a/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java @@ -164,7 +164,8 @@ private Long saveReservation(String name, Long reservationSlotId, String status) name ); jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status) VALUES (?, ?, ?)", + "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " + + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", userId, reservationSlotId, status diff --git a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java index 8d040e5c24..01ccd7d780 100644 --- a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java @@ -141,7 +141,8 @@ private Long saveThemeDateTimeAndReservation(String name) { ); jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status) VALUES (?, ?, ?)", + "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " + + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", userId, reservationId, "CONFIRMED" diff --git a/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java b/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java index 59144788c1..0a558db61a 100644 --- a/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java @@ -105,7 +105,8 @@ void getThemeRankByReservationSlotId() { jdbcTemplate.update("INSERT INTO users(name) VALUES (?)", "보예"); Long userId = jdbcTemplate.queryForObject("SELECT id FROM users WHERE name = ?", Long.class, "보예"); jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status) VALUES (?, ?, ?)", + "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " + + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", userId, targetSlotId, "CONFIRMED" From 06de82132f3f2d79d44faa968f66e7dd677cfc93 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 18 Jun 2026 14:53:39 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20Jdbc=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20JPA=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcReservationRepository.java | 352 ------------------ .../reservation/JpaReservationRepository.java | 83 ++++- .../reservation/ReservationRepository.java | 27 -- .../reservation/ReservationService.java | 73 +++- .../JdbcReservationDateRepository.java | 83 ----- .../ReservationDateRepository.java | 18 - .../JdbcReservationSlotRepository.java | 231 ------------ .../JpaReservationSlotRepository.java | 7 +- .../ReservationSlotRepository.java | 27 -- .../ReservationSlotService.java | 46 ++- .../JdbcReservationTimeRepository.java | 83 ----- .../JpaReservationTimeRepository.java | 2 + .../ReservationTimeRepository.java | 18 - .../domain/theme/JdbcThemeRepository.java | 120 ------ .../domain/theme/ThemeRepository.java | 19 - .../roomescape/domain/theme/ThemeService.java | 62 ++- .../domain/user/JdbcUserRepository.java | 70 ---- .../domain/user/UserRepository.java | 15 - .../ReservationIntegrationTest.java | 125 ++----- .../ReservationDateIntegrationTest.java | 16 +- .../AdminReservationDateIntegrationTest.java | 76 +--- .../ReservationSlotIntegrationTest.java | 123 ++---- .../ReservationSlotServiceTest.java | 31 +- .../AdminReservationSlotIntegrationTest.java | 90 +---- .../AdminReservationTimeIntegrationTest.java | 30 +- .../domain/theme/ThemeIntegrationTest.java | 82 +--- .../domain/theme/ThemeServiceTest.java | 56 ++- .../admin/AdminThemeIntegrationTest.java | 30 +- .../java/roomescape/support/TestFixture.java | 131 +++++++ 29 files changed, 567 insertions(+), 1559 deletions(-) delete mode 100644 src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java delete mode 100644 src/main/java/roomescape/domain/reservation/ReservationRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationslot/JdbcReservationSlotRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationslot/ReservationSlotRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java delete mode 100644 src/main/java/roomescape/domain/theme/JdbcThemeRepository.java delete mode 100644 src/main/java/roomescape/domain/theme/ThemeRepository.java delete mode 100644 src/main/java/roomescape/domain/user/JdbcUserRepository.java delete mode 100644 src/main/java/roomescape/domain/user/UserRepository.java create mode 100644 src/test/java/roomescape/support/TestFixture.java diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java deleted file mode 100644 index 5edfe97eb3..0000000000 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ /dev/null @@ -1,352 +0,0 @@ -package roomescape.domain.reservation; - -import java.sql.PreparedStatement; -import java.sql.Timestamp; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservation.dto.ReservationCountResult; -import roomescape.domain.reservation.dto.ReservationWithWaitingNumber; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationslot.ReservationSlot; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.theme.Theme; -import roomescape.domain.user.User; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationRepository implements ReservationRepository { - - private static final String COLUMN_ID = "user_reservation_id"; - private static final String COLUMN_USER_ID = "user_id"; - private static final String COLUMN_USER_NAME = "user_name"; - private static final String COLUMN_RESERVATION_SLOT_ID = "reservation_slot_id"; - private static final String COLUMN_DATE_ID = "date_id"; - private static final String COLUMN_DATE = "date"; - private static final String COLUMN_TIME_ID = "time_id"; - private static final String COLUMN_START_AT = "start_at"; - private static final String COLUMN_THEME_ID = "theme_id"; - private static final String COLUMN_THEME_NAME = "theme_name"; - private static final String COLUMN_THEME_CONTENT = "theme_content"; - private static final String COLUMN_THEME_URL = "theme_url"; - private static final String COLUMN_WAITING_NUMBER = "waiting_number"; - private static final String COLUMN_STATUS = "status"; - private static final String COLUMN_CREATED_AT = "created_at"; - private static final String COLUMN_UPDATED_AT = "updated_at"; - - private static final String INSERT_SQL = - """ - insert into reservation(reservation_slot_id, user_id, status, created_at, updated_at) - values (?, ?, ?, ?, ?) - """; - private static final String FIND_ALL_WITH_ORDER_AND_STATUS_SQL = - """ - with ranked_reservation as ( - select r.*, - row_number() over ( - partition by r.reservation_slot_id, r.status - order by r.updated_at, r.id - ) as waiting_order - from reservation r - ) - select r.id as user_reservation_id, - case when r.status = 'WAITING' - then r.waiting_order - else null - end as waiting_number, - r.status, - u.id as user_id, - u.name as user_name, - rs.id as reservation_slot_id, - rd.id as date_id, - rd.date, - rt.id as time_id, - rt.start_at, - th.id as theme_id, - th.name as theme_name, - th.content as theme_content, - th.url as theme_url, - r.created_at, - r.updated_at - from ranked_reservation r - join users u on r.user_id = u.id - join reservation_slot rs on r.reservation_slot_id = rs.id - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - order by rd.date desc, rt.start_at desc, r.id; - """; - private static final String FIND_ACTIVE_BY_ID_SQL = - """ - select r.id as user_reservation_id, - r.status, - u.id as user_id, - u.name as user_name, - rs.id as reservation_slot_id, - rd.id as date_id, - rd.date, - rt.id as time_id, - rt.start_at, - th.id as theme_id, - th.name as theme_name, - th.content as theme_content, - th.url as theme_url, - r.created_at, - r.updated_at - from reservation r - join users u on r.user_id = u.id - join reservation_slot rs on r.reservation_slot_id = rs.id - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - where r.id = ? - and r.status <> 'CANCELED' - """; - private static final String FIND_ALL_BY_USERNAME_SQL = - """ - with ranked_reservation as ( - select r.*, - row_number() over ( - partition by r.reservation_slot_id, r.status - order by r.updated_at, r.id - ) as waiting_order - from reservation r - ) - select r.id as user_reservation_id, - case when r.status = 'WAITING' - then r.waiting_order - else null - end as waiting_number, - r.status, - u.id as user_id, - u.name as user_name, - rs.id as reservation_slot_id, - rd.id as date_id, - rd.date, - rt.id as time_id, - rt.start_at, - th.id as theme_id, - th.name as theme_name, - th.content as theme_content, - th.url as theme_url, - r.created_at, - r.updated_at - from ranked_reservation r - join users u on r.user_id = u.id - join reservation_slot rs on r.reservation_slot_id = rs.id - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - where u.name = ? - order by rd.date desc, rt.start_at desc, r.id; - """; - private static final String FIND_RESERVATIONS_IN_WAITING_ORDER_SQL = - """ - select r.id as user_reservation_id, - r.status, - u.id as user_id, - u.name as user_name, - rs.id as reservation_slot_id, - rd.id as date_id, - rd.date, - rt.id as time_id, - rt.start_at, - th.id as theme_id, - th.name as theme_name, - th.content as theme_content, - th.url as theme_url, - r.created_at, - r.updated_at - from reservation r - join users u on r.user_id = u.id - join reservation_slot rs on r.reservation_slot_id = rs.id - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - where r.reservation_slot_id = ? - and r.status <> 'CANCELED' - order by r.updated_at, r.id - """; - private static final String COUNT_BY_RESERVATION_SLOT_ID_SQL = - """ - select count(*) - from reservation - where reservation_slot_id = ? - and status <> 'CANCELED' - """; - private static final String EXISTS_ACTIVE_BY_USER_ID_AND_RESERVATION_ID_SQL = - """ - select exists( - select 1 - from reservation - where user_id = ? - and reservation_slot_id = ? - and status <> 'CANCELED' - ) - """; - private static final String UPDATE_SQL = - """ - update reservation - set reservation_slot_id = ?, user_id = ?, status = ?, created_at = ?, updated_at = ? - where id = ? - """; - private static final String COUNT_RESERVATION_BY_THEME_AND_DATE = - """ - select rt.id as time_id, - rt.start_at, - count(r.id) as reservation_count - from reservation_time rt - left join reservation_slot rs - on rs.time_id = rt.id - and rs.date_id = ? - and rs.theme_id = ? - left join reservation r - on r.reservation_slot_id = rs.id - and r.status <> 'CANCELED' - group by rt.id, rt.start_at - order by rt.start_at; - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Reservation save(Reservation userReservation) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[]{"id"}); - ps.setLong(1, userReservation.getReservationSlot().getId()); - ps.setLong(2, userReservation.getUser().getId()); - ps.setString(3, userReservation.getStatus().name()); - ps.setTimestamp(4, Timestamp.valueOf(userReservation.getCreatedAt())); - ps.setTimestamp(5, Timestamp.valueOf(userReservation.getUpdatedAt())); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return Reservation.createWithId(id, userReservation); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_WITH_ORDER_AND_STATUS_SQL, reservationWithWaitingNumberRowMapper()); - } - - @Override - public Optional findActiveReservation(Long id) { - List result = jdbcTemplate.query(FIND_ACTIVE_BY_ID_SQL, userReservationRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public List findReservations(String username) { - return jdbcTemplate.query(FIND_ALL_BY_USERNAME_SQL, reservationWithWaitingNumberRowMapper(), username); - } - - @Override - public Long countByReservationSlotId(Long reservationSlotId) { - Long count = jdbcTemplate.queryForObject(COUNT_BY_RESERVATION_SLOT_ID_SQL, Long.class, reservationSlotId); - if (count == null) { - return 0L; - } - return count; - } - - @Override - public List findReservationsInWaitingOrder(Long reservationSlotId) { - return jdbcTemplate.query( - FIND_RESERVATIONS_IN_WAITING_ORDER_SQL, - userReservationRowMapper(), - reservationSlotId - ); - } - - @Override - public void update(Long id, Reservation updatedReservation) { - jdbcTemplate.update( - UPDATE_SQL, - updatedReservation.getReservationSlot().getId(), - updatedReservation.getUser().getId(), - updatedReservation.getStatus().name(), - Timestamp.valueOf(updatedReservation.getCreatedAt()), - Timestamp.valueOf(updatedReservation.getUpdatedAt()), - id - ); - } - - @Override - public boolean existsActiveByUserIdAndReservationId(Long userId, Long reservationId) { - Boolean exists = jdbcTemplate.queryForObject( - EXISTS_ACTIVE_BY_USER_ID_AND_RESERVATION_ID_SQL, - Boolean.class, - userId, - reservationId - ); - return exists != null && exists; - } - - @Override - public List countReservation(Long themeId, Long dateId) { - return jdbcTemplate.query( - COUNT_RESERVATION_BY_THEME_AND_DATE, - reservationCountResultRowMapper(), - dateId, - themeId - ); - } - - private RowMapper reservationCountResultRowMapper() { - return (rs, rowNum) -> ReservationCountResult.of( - rs.getLong(COLUMN_TIME_ID), - rs.getTime(COLUMN_START_AT).toLocalTime(), - rs.getLong("reservation_count") - ); - } - - private RowMapper userReservationRowMapper() { - return (rs, rowNum) -> Reservation.of( - rs.getLong(COLUMN_ID), - ReservationSlot.of( - rs.getLong(COLUMN_RESERVATION_SLOT_ID), - ReservationDate.of( - rs.getLong(COLUMN_DATE_ID), - rs.getDate(COLUMN_DATE).toLocalDate() - ), - ReservationTime.of( - rs.getLong(COLUMN_TIME_ID), - rs.getTime(COLUMN_START_AT).toLocalTime() - ), - Theme.of( - rs.getLong(COLUMN_THEME_ID), - rs.getString(COLUMN_THEME_NAME), - rs.getString(COLUMN_THEME_CONTENT), - rs.getString(COLUMN_THEME_URL) - ) - ), - User.of( - rs.getLong(COLUMN_USER_ID), - rs.getString(COLUMN_USER_NAME) - ), - ReservationStatus.valueOf(rs.getString(COLUMN_STATUS)), - rs.getTimestamp(COLUMN_CREATED_AT).toLocalDateTime(), - rs.getTimestamp(COLUMN_UPDATED_AT).toLocalDateTime() - ); - } - - private RowMapper reservationWithWaitingNumberRowMapper() { - return (rs, rowNum) -> new ReservationWithWaitingNumber( - userReservationRowMapper().mapRow(rs, rowNum), - rs.getObject(COLUMN_WAITING_NUMBER, Long.class) - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java index f211e7aef7..0fc9999a98 100644 --- a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java @@ -1,10 +1,11 @@ package roomescape.domain.reservation; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; +import roomescape.domain.theme.Theme; public interface JpaReservationRepository extends JpaRepository { @@ -16,10 +17,7 @@ select count(reservation) where reservation.reservationSlot.id = :reservationSlotId and reservation.status <> :excludedStatus """) - Long countActiveByReservationSlotId( - @Param("reservationSlotId") Long reservationSlotId, - @Param("excludedStatus") ReservationStatus excludedStatus - ); + Long countActiveReservationsInSlot(Long reservationSlotId, ReservationStatus excludedStatus); boolean existsByUserIdAndReservationSlotIdAndStatusNot( Long userId, @@ -31,11 +29,78 @@ boolean existsByUserIdAndReservationSlotIdAndStatusNot( select reservation from Reservation reservation where reservation.reservationSlot.id = :reservationSlotId - and reservation.status <> :excludedStatus + and reservation.status = :status order by reservation.updatedAt, reservation.id """) - List findActiveReservationsInWaitingOrder( - @Param("reservationSlotId") Long reservationSlotId, - @Param("excludedStatus") ReservationStatus excludedStatus + List findWaitingReservationsForPromotion( + Long reservationSlotId, + ReservationStatus status + ); + + @Query(""" + select reservation + from Reservation reservation + join fetch reservation.user + join fetch reservation.reservationSlot slot + join fetch slot.date date + join fetch slot.time time + join fetch slot.theme + order by date.date desc, time.startAt desc, reservation.id + """) + List findReservationsForAdmin(); + + @Query(""" + select reservation + from Reservation reservation + join fetch reservation.user user + join fetch reservation.reservationSlot slot + join fetch slot.date date + join fetch slot.time time + join fetch slot.theme + where user.name = :username + order by date.date desc, time.startAt desc, reservation.id + """) + List findUserReservations(String username); + + @Query(""" + select reservation + from Reservation reservation + join fetch reservation.reservationSlot slot + where slot.id in :reservationSlotIds + and reservation.status = :status + order by slot.id, reservation.updatedAt, reservation.id + """) + List findWaitingReservationsInSlots( + List reservationSlotIds, + ReservationStatus status + ); + + @Query(""" + select reservation + from Reservation reservation + join fetch reservation.reservationSlot slot + join fetch slot.time + where slot.theme.id = :themeId + and slot.date.id = :dateId + and reservation.status <> :excludedStatus + """) + List findReservationsForSlotAvailability( + Long themeId, + Long dateId, + ReservationStatus excludedStatus + ); + + @Query(""" + select slot.theme + from Reservation reservation + join reservation.reservationSlot slot + join slot.date date + where date.date between :startDay and :today + and reservation.status <> :excludedStatus + """) + List findThemesForRanking( + LocalDate startDay, + LocalDate today, + ReservationStatus excludedStatus ); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java deleted file mode 100644 index c15c802b15..0000000000 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package roomescape.domain.reservation; - -import java.util.List; -import java.util.Optional; -import roomescape.domain.reservation.dto.ReservationCountResult; -import roomescape.domain.reservation.dto.ReservationWithWaitingNumber; - -public interface ReservationRepository { - - Reservation save(Reservation userReservation); - - List findAll(); - - Optional findActiveReservation(Long id); - - List findReservations(String username); - - Long countByReservationSlotId(Long reservationSlotId); - - List findReservationsInWaitingOrder(Long reservationSlotId); - - void update(Long id, Reservation updatedReservation); - - boolean existsActiveByUserIdAndReservationId(Long userId, Long reservationId); - - List countReservation(Long themeId, Long dateId); -} diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index b412165712..bd9b0f69e4 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -4,7 +4,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,7 +39,6 @@ public class ReservationService { private final JpaReservationRepository reservationRepository; - private final ReservationRepository reservationQueryRepository; private final UserService userService; private final ReservationSlotService reservationSlotService; private final ThemeService themeService; @@ -59,16 +61,70 @@ public CreateReservationResponse createReservation(CreateReservationRequest requ } public List getAllReservations() { - return reservationQueryRepository.findAll().stream() + return findAllWithWaitingNumber().stream() .map(ReservationResponse::from) .toList(); } public UserReservationsResponse getUserReservations(String username) { - List userReservations = reservationQueryRepository.findReservations(username); + List reservations = reservationRepository.findUserReservations(username); + Map waitingNumberByReservationId = calculateWaitingNumbersInSameSlots(reservations); + List userReservations = reservations.stream() + .map(reservation -> new ReservationWithWaitingNumber( + reservation, + waitingNumberByReservationId.get(reservation.getId()) + )) + .toList(); return UserReservationsResponse.of(username, userReservations); } + private List findAllWithWaitingNumber() { + List reservations = reservationRepository.findReservationsForAdmin(); + Map waitingNumberByReservationId = calculateWaitingNumbers(reservations); + return reservations.stream() + .map(reservation -> new ReservationWithWaitingNumber( + reservation, + waitingNumberByReservationId.get(reservation.getId()) + )) + .toList(); + } + + private Map calculateWaitingNumbers(List reservations) { + Map waitingNumberByReservationId = new HashMap<>(); + Map nextWaitingNumberBySlotId = new HashMap<>(); + + reservations.stream() + .filter(reservation -> reservation.getStatus() == ReservationStatus.WAITING) + .sorted(Comparator + .comparing((Reservation reservation) -> reservation.getReservationSlot().getId()) + .thenComparing(Reservation::getUpdatedAt) + .thenComparing(Reservation::getId)) + .forEach(reservation -> { + Long slotId = reservation.getReservationSlot().getId(); + Long waitingNumber = nextWaitingNumberBySlotId.merge(slotId, 1L, Long::sum); + waitingNumberByReservationId.put(reservation.getId(), waitingNumber); + }); + + return waitingNumberByReservationId; + } + + private Map calculateWaitingNumbersInSameSlots(List reservations) { + List reservationSlotIds = reservations.stream() + .map(reservation -> reservation.getReservationSlot().getId()) + .distinct() + .toList(); + + if (reservationSlotIds.isEmpty()) { + return Map.of(); + } + + List waitingReservations = reservationRepository.findWaitingReservationsInSlots( + reservationSlotIds, + ReservationStatus.WAITING + ); + return calculateWaitingNumbers(waitingReservations); + } + @Transactional public void cancelReservationByAdmin(Long id) { Reservation reservation = findActiveReservationByIdOrThrow(id); @@ -144,7 +200,7 @@ private boolean hasSameReservationSlot(ReservationSlot oldSlot, ReservationSlot } private void updateReservationWhenSameSlot(Reservation reservation, ReservationSlot currentReservationSlot) { - Long currentReservationCount = reservationRepository.countActiveByReservationSlotId( + Long currentReservationCount = reservationRepository.countActiveReservationsInSlot( currentReservationSlot.getId(), ReservationStatus.CANCELED ); @@ -168,7 +224,7 @@ private void updateReservationWhenMovingSlot( ReservationSlot currentSlot, ReservationSlot updatedSlot ) { - Long currentReservationCount = reservationRepository.countActiveByReservationSlotId( + Long currentReservationCount = reservationRepository.countActiveReservationsInSlot( updatedSlot.getId(), ReservationStatus.CANCELED ); @@ -188,11 +244,10 @@ private void updateReservationWhenMovingSlot( } private void promoteFirstWaitingReservation(ReservationSlot reservationSlot) { - reservationRepository.findActiveReservationsInWaitingOrder( + reservationRepository.findWaitingReservationsForPromotion( reservationSlot.getId(), - ReservationStatus.CANCELED + ReservationStatus.WAITING ).stream() - .filter(reservation -> reservation.getStatus() == ReservationStatus.WAITING) .findFirst() .ifPresent(reservation -> reservationRepository.save( reservation.update(ReservationStatus.CONFIRMED, clock) @@ -200,7 +255,7 @@ private void promoteFirstWaitingReservation(ReservationSlot reservationSlot) { } private Reservation buildReservation(ReservationSlot reservationSlot, User user) { - Long currentReservationCount = reservationRepository.countActiveByReservationSlotId( + Long currentReservationCount = reservationRepository.countActiveReservationsInSlot( reservationSlot.getId(), ReservationStatus.CANCELED ); diff --git a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java deleted file mode 100644 index a5b1927132..0000000000 --- a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java +++ /dev/null @@ -1,83 +0,0 @@ -package roomescape.domain.reservationdate; - -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.errors.RoomescapeErrors; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationDateRepository implements ReservationDateRepository { - - private static final String COLUMN_ID = "id"; - private static final String COLUMN_DATE = "date"; - - private static final String INSERT_SQL = "insert into reservation_date(`date`) values (?)"; - private static final String FIND_BY_ID_SQL = "select id, `date` from reservation_date where id = ?"; - private static final String FIND_BY_DATE_SQL = "select id, `date` from reservation_date where `date` = ?"; - private static final String FIND_ALL_SQL = "select id, `date` from reservation_date order by id"; - private static final String DELETE_BY_ID_SQL = "delete from reservation_date where id = ?"; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, reservationDateRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, reservationDateRowMapper()); - } - - @Override - public ReservationDate save(ReservationDate reservationDate) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setDate(1, Date.valueOf(reservationDate.getDate())); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return ReservationDate.of( - id, - reservationDate.getDate() - ); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public Optional findByDate(LocalDate startWhen) { - List result = jdbcTemplate.query(FIND_BY_DATE_SQL, reservationDateRowMapper(), startWhen); - return result.stream().findFirst(); - } - - private RowMapper reservationDateRowMapper() { - return (rs, rowNum) -> ReservationDate.of( - rs.getLong(COLUMN_ID), - rs.getDate(COLUMN_DATE).toLocalDate() - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new InternalServerException(RoomescapeErrors.INVALID_GENERATED_KEY); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java deleted file mode 100644 index 7f245d2125..0000000000 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package roomescape.domain.reservationdate; - -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; - -public interface ReservationDateRepository { - - Optional findById(Long id); - - List findAll(); - - ReservationDate save(ReservationDate reservationDate); - - int deleteById(Long id); - - Optional findByDate(LocalDate startWhen); -} diff --git a/src/main/java/roomescape/domain/reservationslot/JdbcReservationSlotRepository.java b/src/main/java/roomescape/domain/reservationslot/JdbcReservationSlotRepository.java deleted file mode 100644 index 7f08955c44..0000000000 --- a/src/main/java/roomescape/domain/reservationslot/JdbcReservationSlotRepository.java +++ /dev/null @@ -1,231 +0,0 @@ -package roomescape.domain.reservationslot; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.theme.Theme; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationSlotRepository implements ReservationSlotRepository { - - private static final String COLUMN_ID = "id"; - private static final String COLUMN_DATE_ID = "date_id"; - private static final String COLUMN_DATE = "date"; - private static final String COLUMN_TIME_ID = "time_id"; - private static final String COLUMN_START_AT = "start_at"; - private static final String COLUMN_THEME_ID = "theme_id"; - private static final String COLUMN_THEME_NAME = "theme_name"; - private static final String COLUMN_THEME_CONTENT = "theme_content"; - private static final String COLUMN_THEME_URL = "theme_url"; - - private static final String INSERT_SQL = "insert into reservation_slot(date_id, time_id, theme_id) values (?, ?, ?)"; - private static final String FIND_ALL_SQL = - """ - select rs.id, - rd.id as date_id, rd.date, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation_slot rs - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - order by rs.id - """; - private static final String COUNT_BY_TIME_ID_SQL = - """ - select count(*) - from reservation_slot - where time_id = ? - """; - private static final String COUNT_BY_RESERVATION_DATE_ID_SQL = - """ - select count(*) - from reservation_slot - where date_id = ? - """; - private static final String DELETE_BY_ID_SQL = "delete from reservation_slot where id = ?"; - private static final String COUNT_BY_THEME_ID_SQL = - """ - select count(*) - from reservation_slot - where theme_id = ? - """; - private static final String FIND_BY_SCHEDULE_SQL = - """ - select rs.id, - rd.id as date_id, rd.date, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation_slot rs - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - where rs.time_id = ? and rs.date_id = ? and rs.theme_id = ? - """; - private static final String FIND_BY_ID_SQL = - """ - select rs.id, - rd.id as date_id, rd.date, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation_slot rs - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - where rs.id = ? - """; - private static final String UPDATE_SQL = - """ - update reservation_slot - set date_id = ?, time_id = ?, theme_id = ? - where id = ? - """; - private static final String FIND_BY_SCHEDULE_WITH_LOCK_SQL = - """ - select rs.id, - rd.id as date_id, rd.date, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation_slot rs - join reservation_date rd on rs.date_id = rd.id - join reservation_time rt on rs.time_id = rt.id - join theme th on rs.theme_id = th.id - where rs.time_id = ? and rs.date_id = ? and rs.theme_id = ? - for update - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public ReservationSlot save(ReservationSlot reservation) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setLong(1, reservation.getDate().getId()); - ps.setLong(2, reservation.getTime().getId()); - ps.setLong(3, reservation.getTheme().getId()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return ReservationSlot.createWithId(id, reservation); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, reservationRowMapper()); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public int countByTimeId(Long timeId) { - Integer count = jdbcTemplate.queryForObject(COUNT_BY_TIME_ID_SQL, Integer.class, timeId); - if (count == null) { - return 0; - } - return count; - } - - @Override - public int countByReservationDateId(Long dateId) { - Integer count = jdbcTemplate.queryForObject(COUNT_BY_RESERVATION_DATE_ID_SQL, Integer.class, dateId); - if (count == null) { - return 0; - } - return count; - } - - @Override - public int countByThemeId(Long themeId) { - Integer count = jdbcTemplate.queryForObject(COUNT_BY_THEME_ID_SQL, Integer.class, themeId); - if (count == null) { - return 0; - } - return count; - } - - @Override - public Optional findBySchedule(Long timeId, Long dateId, Long themeId) { - List result = jdbcTemplate.query( - FIND_BY_SCHEDULE_SQL, - reservationRowMapper(), - timeId, - dateId, - themeId - ); - return result.stream().findFirst(); - } - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, reservationRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public Optional update(Long id, ReservationSlot withoutId) { - int updatedCount = jdbcTemplate.update( - UPDATE_SQL, - withoutId.getDate().getId(), - withoutId.getTime().getId(), - withoutId.getTheme().getId(), - id - ); - if (updatedCount == 0) { - return Optional.empty(); - } - return findById(id); - } - - @Override - public Optional findByScheduleToUpdate(Long timeId, Long dateId, Long themeId) { - List result = jdbcTemplate.query( - FIND_BY_SCHEDULE_WITH_LOCK_SQL, - reservationRowMapper(), - timeId, - dateId, - themeId - ); - return result.stream().findFirst(); - } - - private RowMapper reservationRowMapper() { - return (rs, rowNum) -> ReservationSlot.of( - rs.getLong(COLUMN_ID), - ReservationDate.of( - rs.getLong(COLUMN_DATE_ID), - rs.getDate(COLUMN_DATE).toLocalDate() - ), - ReservationTime.of( - rs.getLong(COLUMN_TIME_ID), - rs.getTime(COLUMN_START_AT).toLocalTime() - ), - Theme.of( - rs.getLong(COLUMN_THEME_ID), - rs.getString(COLUMN_THEME_NAME), - rs.getString(COLUMN_THEME_CONTENT), - rs.getString(COLUMN_THEME_URL) - ) - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java b/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java index 64b6d28e7a..b2f5b6aa88 100644 --- a/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java +++ b/src/main/java/roomescape/domain/reservationslot/JpaReservationSlotRepository.java @@ -5,7 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; public interface JpaReservationSlotRepository extends JpaRepository { @@ -23,9 +22,5 @@ public interface JpaReservationSlotRepository extends JpaRepository findByScheduleToUpdate( - @Param("timeId") Long timeId, - @Param("dateId") Long dateId, - @Param("themeId") Long themeId - ); + Optional findSlotForCreation(Long timeId, Long dateId, Long themeId); } diff --git a/src/main/java/roomescape/domain/reservationslot/ReservationSlotRepository.java b/src/main/java/roomescape/domain/reservationslot/ReservationSlotRepository.java deleted file mode 100644 index 8fc4aec0a4..0000000000 --- a/src/main/java/roomescape/domain/reservationslot/ReservationSlotRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package roomescape.domain.reservationslot; - -import java.util.List; -import java.util.Optional; - -public interface ReservationSlotRepository { - - ReservationSlot save(ReservationSlot reservation); - - List findAll(); - - int deleteById(Long id); - - int countByTimeId(Long timeId); - - int countByReservationDateId(Long dateId); - - int countByThemeId(Long id); - - Optional findBySchedule(Long timeId, Long dateId, Long themeId); - - Optional findById(Long id); - - Optional update(Long id, ReservationSlot withoutId); - - Optional findByScheduleToUpdate(Long timeId, Long dateId, Long themeId); -} diff --git a/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java b/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java index 3163d841bd..b4aab8af75 100644 --- a/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java +++ b/src/main/java/roomescape/domain/reservationslot/ReservationSlotService.java @@ -1,14 +1,19 @@ package roomescape.domain.reservationslot; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; -import roomescape.domain.reservation.ReservationRepository; +import roomescape.domain.reservation.JpaReservationRepository; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationStatus; import roomescape.domain.reservation.dto.ReservationCountResult; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.JpaReservationDateRepository; import roomescape.domain.reservationslot.dto.ReservationSlotResponse; +import roomescape.domain.reservationtime.JpaReservationTimeRepository; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; import roomescape.domain.theme.JpaThemeRepository; @@ -24,30 +29,55 @@ public class ReservationSlotService { private final JpaReservationSlotRepository reservationSlotRepository; private final JpaThemeRepository themeRepository; private final JpaReservationDateRepository reservationDateRepository; - private final ReservationRepository reservationRepository; + private final JpaReservationRepository reservationRepository; + private final JpaReservationTimeRepository reservationTimeRepository; public List getReservationSlots(Long themeId, Long dateId) { validateThemeAndDateExists(themeId, dateId); - List reservationCountResults = reservationRepository.countReservation(themeId, dateId); + List reservationTimes = reservationTimeRepository.findAllByOrderByStartAtAsc(); + List reservations = reservationRepository.findReservationsForSlotAvailability( + themeId, + dateId, + ReservationStatus.CANCELED + ); - return reservationCountResults.stream() + return createReservationCountResults(reservationTimes, reservations).stream() .map(ReservationSlotResponse::from) .toList(); } + private List createReservationCountResults( + List reservationTimes, + List reservations + ) { + Map reservationCountByTimeId = reservations.stream() + .collect(Collectors.groupingBy( + reservation -> reservation.getReservationSlot().getTime().getId(), + Collectors.counting() + )); + + return reservationTimes.stream() + .map(reservationTime -> ReservationCountResult.of( + reservationTime.getId(), + reservationTime.getStartAt(), + reservationCountByTimeId.getOrDefault(reservationTime.getId(), 0L) + )) + .toList(); + } + public ReservationSlot findOrCreateReservationSlot( ReservationDate reservationDate, ReservationTime reservationTime, Theme theme ) { try { - return reservationSlotRepository.findByScheduleToUpdate( + return reservationSlotRepository.findSlotForCreation( reservationTime.getId(), reservationDate.getId(), theme.getId() ).orElseGet(() -> saveReservationSlot(reservationDate, reservationTime, theme)); } catch (DataIntegrityViolationException e) { - return reservationSlotRepository.findByScheduleToUpdate( + return reservationSlotRepository.findSlotForCreation( reservationTime.getId(), reservationDate.getId(), theme.getId() @@ -70,8 +100,4 @@ private void validateThemeAndDateExists(Long themeId, Long dateId) { reservationDateRepository.findById(dateId) .orElseThrow(() -> new NotFoundException(ReservationDateErrors.RESERVATION_DATE_NOT_EXIST)); } - - public void deleteReservationSlot(Long id) { - reservationSlotRepository.deleteById(id); - } } diff --git a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java deleted file mode 100644 index 1207ccb831..0000000000 --- a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java +++ /dev/null @@ -1,83 +0,0 @@ -package roomescape.domain.reservationtime; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.sql.Time; -import java.time.LocalTime; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.errors.RoomescapeErrors; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationTimeRepository implements ReservationTimeRepository { - - private static final String COLUMN_ID = "id"; - private static final String COLUMN_START_AT = "start_at"; - - private static final String INSERT_SQL = "insert into reservation_time(start_at) values (?)"; - private static final String FIND_ALL_SQL = "select id, start_at from reservation_time order by id"; - private static final String FIND_BY_ID_SQL = "select id, start_at from reservation_time where id = ?"; - private static final String FIND_BY_TIME_SQL = "select id, start_at from reservation_time where start_at = ?"; - private static final String DELETE_BY_ID_SQL = "delete from reservation_time where id = ?"; - - private final JdbcTemplate jdbcTemplate; - - @Override - public ReservationTime save(ReservationTime reservationTime) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setTime(1, Time.valueOf(reservationTime.getStartAt())); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return ReservationTime.of( - id, - reservationTime.getStartAt() - ); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, reservationTimeRowMapper()); - } - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, reservationTimeRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public Optional findByStartAt(LocalTime startAt) { - List result = jdbcTemplate.query(FIND_BY_TIME_SQL, reservationTimeRowMapper(), startAt); - return result.stream().findFirst(); - } - - private RowMapper reservationTimeRowMapper() { - return (rs, rowNum) -> ReservationTime.of( - rs.getLong(COLUMN_ID), - rs.getTime(COLUMN_START_AT).toLocalTime() - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new InternalServerException(RoomescapeErrors.INVALID_GENERATED_KEY); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java index 07e64f5612..e9a94d0a30 100644 --- a/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservationtime/JpaReservationTimeRepository.java @@ -1,7 +1,9 @@ package roomescape.domain.reservationtime; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface JpaReservationTimeRepository extends JpaRepository { + List findAllByOrderByStartAtAsc(); } diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java deleted file mode 100644 index c9c4b55eb2..0000000000 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package roomescape.domain.reservationtime; - -import java.time.LocalTime; -import java.util.List; -import java.util.Optional; - -public interface ReservationTimeRepository { - - ReservationTime save(ReservationTime reservationTime); - - List findAll(); - - Optional findById(Long id); - - int deleteById(Long id); - - Optional findByStartAt(LocalTime startAt); -} diff --git a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java b/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java deleted file mode 100644 index a9477fca9a..0000000000 --- a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java +++ /dev/null @@ -1,120 +0,0 @@ -package roomescape.domain.theme; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.theme.dto.ThemeRankResult; -import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.errors.RoomescapeErrors; - -@Repository -@RequiredArgsConstructor -public class JdbcThemeRepository implements ThemeRepository { - - private static final String COLUMN_ID = "id"; - private static final String COLUMN_NAME = "name"; - private static final String COLUMN_CONTENT = "content"; - private static final String COLUMN_URL = "url"; - - private static final String FIND_ALL_SQL = "select id, name, content, url from theme order by id"; - private static final String FIND_BY_ID_SQL = "select id, name, content, url from theme where id = ?"; - private static final String INSERT_SQL = "insert into theme(name, content, url) values (?, ?, ?)"; - private static final String DELETE_BY_ID_SQL = "delete from theme where id = ?"; - private static final String FIND_ALL_WITH_RANK_SQL = """ - select - t.id, - t.name, - t.url, - rank() over (order by count(r.id) desc) as rank - from theme t - join reservation_slot rs on rs.theme_id = t.id - join reservation r on r.reservation_slot_id = rs.id - join reservation_date rd on rd.id = rs.date_id - where rd.date between ? and ? - and r.status <> 'CANCELED' - group by t.id, t.name, t.url - order by rank, t.id - limit ? - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, themeRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, themeRowMapper()); - } - - @Override - public List findPopularThemes(int rankLimit, LocalDate startDay, LocalDate today) { - return jdbcTemplate.query( - FIND_ALL_WITH_RANK_SQL, - themeRankRowMapper(), - startDay, - today, - rankLimit - ); - } - - @Override - public Theme save(Theme theme) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getContent()); - ps.setString(3, theme.getUrl()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return Theme.of( - id, - theme.getName(), - theme.getContent(), - theme.getUrl() - ); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - private RowMapper themeRowMapper() { - return ((rs, rowNum) -> Theme.of( - rs.getLong(COLUMN_ID), - rs.getString(COLUMN_NAME), - rs.getString(COLUMN_CONTENT), - rs.getString(COLUMN_URL) - )); - } - - private RowMapper themeRankRowMapper() { - return ((rs, rowNum) -> ThemeRankResult.of( - rs.getLong(COLUMN_ID), - rs.getString(COLUMN_NAME), - rs.getString(COLUMN_URL), - rs.getInt("rank") - )); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new InternalServerException(RoomescapeErrors.INVALID_GENERATED_KEY); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/theme/ThemeRepository.java b/src/main/java/roomescape/domain/theme/ThemeRepository.java deleted file mode 100644 index d4197a87f0..0000000000 --- a/src/main/java/roomescape/domain/theme/ThemeRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package roomescape.domain.theme; - -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import roomescape.domain.theme.dto.ThemeRankResult; - -public interface ThemeRepository { - - Optional findById(Long id); - - List findAll(); - - List findPopularThemes(int rankLimit, LocalDate startDay, LocalDate today); - - Theme save(Theme theme); - - int deleteById(Long id); -} diff --git a/src/main/java/roomescape/domain/theme/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index cfb5031542..9aac0b5df8 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -2,9 +2,17 @@ import java.time.Clock; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import roomescape.domain.reservation.JpaReservationRepository; +import roomescape.domain.reservation.ReservationStatus; import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.theme.admin.dto.AdminThemeResponse; import roomescape.domain.theme.admin.dto.CreateThemeRequest; @@ -25,7 +33,7 @@ public class ThemeService { private final JpaThemeRepository themeRepository; private final JpaReservationSlotRepository reservationSlotRepository; - private final ThemeRepository themeQueryRepository; + private final JpaReservationRepository reservationRepository; private final Clock clock; @@ -56,12 +64,60 @@ public List getAllTheme() { public List getThemeRank() { LocalDate today = LocalDate.now(clock); LocalDate startDay = today.minusDays(RANK_DAYS_LIMIT); - List popularThemes = themeQueryRepository.findPopularThemes(RANK_LIMIT, startDay, today); - return popularThemes.stream() + List reservedThemes = reservationRepository.findThemesForRanking( + startDay, + today, + ReservationStatus.CANCELED + ); + return calculateThemeRanks(reservedThemes).stream() + .limit(RANK_LIMIT) .map(ThemeRankResponse::from) .toList(); } + private List calculateThemeRanks(List reservedThemes) { + Map reservationCountByThemeId = reservedThemes.stream() + .collect(Collectors.groupingBy( + Theme::getId, + Collectors.counting() + )); + Map themeById = reservedThemes.stream() + .collect(Collectors.toMap( + Theme::getId, + Function.identity(), + (firstTheme, ignored) -> firstTheme, + LinkedHashMap::new + )); + + List> sortedEntries = reservationCountByThemeId.entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()) + .thenComparing(Map.Entry.comparingByKey())) + .toList(); + + List results = new ArrayList<>(); + Long previousCount = null; + int rank = 0; + + for (int index = 0; index < sortedEntries.size(); index++) { + Map.Entry entry = sortedEntries.get(index); + Long reservationCount = entry.getValue(); + if (!reservationCount.equals(previousCount)) { + rank = index + 1; + previousCount = reservationCount; + } + + Theme theme = themeById.get(entry.getKey()); + results.add(ThemeRankResult.of( + theme.getId(), + theme.getName(), + theme.getUrl(), + rank + )); + } + + return results; + } + public Theme findThemeByIdOrThrow(Long themeId) { return themeRepository.findById(themeId) .orElseThrow(() -> new NotFoundException(ThemeErrors.THEME_NOT_EXIST)); diff --git a/src/main/java/roomescape/domain/user/JdbcUserRepository.java b/src/main/java/roomescape/domain/user/JdbcUserRepository.java deleted file mode 100644 index 23da1f07bb..0000000000 --- a/src/main/java/roomescape/domain/user/JdbcUserRepository.java +++ /dev/null @@ -1,70 +0,0 @@ -package roomescape.domain.user; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class JdbcUserRepository implements UserRepository { - - private static final String COLUMN_ID = "id"; - private static final String COLUMN_NAME = "name"; - - private static final String INSERT_SQL = "insert into users(name) values (?)"; - private static final String FIND_ALL_SQL = "select id, name from users order by id"; - private static final String FIND_BY_ID_SQL = "select id, name from users where id = ?"; - private static final String FIND_BY_NAME_SQL = "select id, name from users where name = ?"; - - private final JdbcTemplate jdbcTemplate; - - @Override - public User save(User user) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, user.getName()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return User.createWithId(id, user); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, userRowMapper()); - } - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, userRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public Optional findByName(String name) { - List result = jdbcTemplate.query(FIND_BY_NAME_SQL, userRowMapper(), name); - return result.stream().findFirst(); - } - - private RowMapper userRowMapper() { - return (rs, rowNum) -> User.of( - rs.getLong(COLUMN_ID), - rs.getString(COLUMN_NAME) - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/user/UserRepository.java b/src/main/java/roomescape/domain/user/UserRepository.java deleted file mode 100644 index eac1385bb0..0000000000 --- a/src/main/java/roomescape/domain/user/UserRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package roomescape.domain.user; - -import java.util.List; -import java.util.Optional; - -public interface UserRepository { - - User save(User user); - - List findAll(); - - Optional findById(Long id); - - Optional findByName(String name); -} diff --git a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java index 1372fd60ae..18f18ee46a 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java @@ -24,8 +24,11 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ReservationIntegrationTest { @@ -34,7 +37,7 @@ class ReservationIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @MockitoSpyBean private JpaReservationRepository reservationRepository; @@ -58,12 +61,7 @@ Clock fixedClock() { @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @@ -146,6 +144,28 @@ void getUserReservations() { .body("reservations[0].status", is("CONFIRMED")); } + @Test + @DisplayName("사용자 예약 조회 시 같은 슬롯의 대기 순서를 기준으로 대기번호를 계산한다.") + void getUserReservationsWithWaitingNumberInSameSlot() { + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-06-01"); + Long timeId = saveTime("10:00"); + Long reservationSlotId = saveReservationSlot(dateId, timeId, themeId); + saveReservation("보예", reservationSlotId, ReservationStatus.CONFIRMED); + saveReservation("수민", reservationSlotId, ReservationStatus.WAITING); + saveReservation("말랑", reservationSlotId, ReservationStatus.WAITING); + + given().log().all() + .contentType(ContentType.JSON) + .param("name", "말랑") + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("username", is("말랑")) + .body("reservations[0].status", is("WAITING")) + .body("reservations[0].waitingNumber", is(2)); + } + @Test @DisplayName("예약 조회 시 이름 파라미터가 누락되었을 경우 400 에러가 발생한다.") void getUserReservationsWithoutName() { @@ -216,8 +236,8 @@ void checkRollback() { .statusCode(500); assertSoftly(softly -> { - assertThat(findReservationStatus(confirmedReservationId)).isEqualTo(ReservationStatus.CONFIRMED.toString()); - assertThat(findReservationStatus(waitingReservationId)).isEqualTo(ReservationStatus.WAITING.toString()); + assertThat(findReservationStatus(confirmedReservationId)).isEqualTo(ReservationStatus.CONFIRMED); + assertThat(findReservationStatus(waitingReservationId)).isEqualTo(ReservationStatus.WAITING); } ); } @@ -254,96 +274,33 @@ void updateReservation() { } private Long saveReservation(String name, String date, String time, String themeName) { - Long themeId = saveTheme(themeName); - Long dateId = saveDate(date); - Long timeId = saveTime(time); - - Long reservationSlotId = saveReservationSlot(dateId, timeId, themeId); - return saveReservation(name, reservationSlotId, ReservationStatus.CONFIRMED); + return testFixture.saveReservation(name, date, time, themeName).getId(); } private Long saveReservation(String name, Long reservationSlotId, ReservationStatus status) { - jdbcTemplate.update("INSERT INTO users(name) VALUES (?)", name); - Long userId = jdbcTemplate.queryForObject( - "SELECT id FROM users WHERE name = ?", - Long.class, - name - ); - jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " - + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", - userId, - reservationSlotId, - status.name() - ); - - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation WHERE user_id = ? AND reservation_slot_id = ?", - Long.class, - userId, - reservationSlotId - ); + return testFixture.saveReservation(name, reservationSlotId, status).getId(); } private Long saveReservationSlot(Long dateId, Long timeId, Long themeId) { - jdbcTemplate.update( - "INSERT INTO reservation_slot(date_id, time_id, theme_id) VALUES (?, ?, ?)", - dateId, - timeId, - themeId - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation_slot WHERE date_id = ? AND time_id = ? AND theme_id = ?", - Long.class, - dateId, - timeId, - themeId - ); + return testFixture.saveSlot(dateId, timeId, themeId).getId(); } - private String findReservationStatus(Long reservationId) { - return jdbcTemplate.queryForObject( - "SELECT status FROM reservation WHERE id = ?", - String.class, - reservationId - ); + private ReservationStatus findReservationStatus(Long reservationId) { + return testFixture.findReservationStatus(reservationId); } private Long saveTheme(String themeName) { - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - themeName, - "무서운 테마", - "theme-url" - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM theme WHERE name = ?", - Long.class, - themeName - ); + Theme theme = testFixture.saveTheme(themeName); + return theme.getId(); } private Long saveDate(String date) { - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - date - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation_date WHERE date = ?", - Long.class, - date - ); + ReservationDate reservationDate = testFixture.saveDate(date); + return reservationDate.getId(); } private Long saveTime(String time) { - jdbcTemplate.update( - "INSERT INTO reservation_time(start_at) VALUES (?)", - time - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation_time WHERE start_at = ?", - Long.class, - time + ":00" - ); + ReservationTime reservationTime = testFixture.saveTime(time); + return reservationTime.getId(); } } diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java index b9b9d4e285..9623d70717 100644 --- a/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ReservationDateIntegrationTest { @@ -21,26 +21,18 @@ class ReservationDateIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @DisplayName("전체 예약 날짜 조회를 end-to-end로 확인한다.") void getAllReservationDates() { - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - "2026-06-01" - ); + testFixture.saveDate("2026-06-01"); given().log().all() .contentType(ContentType.JSON) diff --git a/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java b/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java index 458ce1a519..664da3077d 100644 --- a/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java @@ -15,7 +15,11 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationslot.ReservationSlot; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class AdminReservationDateIntegrationTest { @@ -24,7 +28,7 @@ class AdminReservationDateIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @Value("${token}") private String adminToken; @@ -32,21 +36,13 @@ class AdminReservationDateIntegrationTest { @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @DisplayName("관리자의 예약 날짜 전체 조회를 end-to-end로 확인한다.") void getAllReservationDateForAdmin() { - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - "2026-06-01" - ); + testFixture.saveDate("2026-06-01"); given().log().all() .contentType(ContentType.JSON) @@ -133,21 +129,12 @@ void createReservationDateWithoutToken() { @Test @DisplayName("관리자의 예약 날짜 삭제를 end-to-end로 확인한다.") void deleteReservationDate() { - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - "2026-06-01" - ); - - Long dateId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_date WHERE date = ?", - Long.class, - "2026-06-01" - ); + ReservationDate reservationDate = testFixture.saveDate("2026-06-01"); given().log().all() .contentType(ContentType.JSON) .header("X-ADMIN-TOKEN", adminToken) - .when().delete("/admin/reservation-dates/{id}", dateId) + .when().delete("/admin/reservation-dates/{id}", reservationDate.getId()) .then().log().all() .statusCode(204); @@ -163,48 +150,15 @@ void deleteReservationDate() { @Test @DisplayName("이미 예약이 존재하는 날짜는 삭제할 수 없다.") void deleteReservationDateWhenDateInUse() { - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - "2026-06-01" - ); - jdbcTemplate.update( - "INSERT INTO reservation_time(start_at) VALUES (?)", - "10:00" - ); - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "공포", - "무서운 테마", - "theme-url" - ); - - Long dateId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_date WHERE date = ?", - Long.class, - "2026-06-01" - ); - Long timeId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_time WHERE start_at = ?", - Long.class, - "10:00:00" - ); - Long themeId = jdbcTemplate.queryForObject( - "SELECT id FROM theme WHERE name = ?", - Long.class, - "공포" - ); - - jdbcTemplate.update( - "INSERT INTO reservation_slot(date_id, time_id, theme_id) VALUES (?, ?, ?)", - dateId, - timeId, - themeId - ); + ReservationDate reservationDate = testFixture.saveDate("2026-06-01"); + ReservationTime reservationTime = testFixture.saveTime("10:00"); + Theme theme = testFixture.saveTheme("공포"); + ReservationSlot reservationSlot = testFixture.saveSlot(reservationDate, reservationTime, theme); given().log().all() .contentType(ContentType.JSON) .header("X-ADMIN-TOKEN", adminToken) - .when().delete("/admin/reservation-dates/{id}", dateId) + .when().delete("/admin/reservation-dates/{id}", reservationSlot.getDate().getId()) .then().log().all() .statusCode(409) .body("code", is("RESERVATION_DATE_IN_USE")) diff --git a/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java b/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java index cd3ff4081c..e09b3b1da0 100644 --- a/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationslot/ReservationSlotIntegrationTest.java @@ -12,7 +12,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.reservation.ReservationStatus; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ReservationSlotIntegrationTest { @@ -21,40 +25,35 @@ class ReservationSlotIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @DisplayName("예약 슬롯 조회를 end-to-end로 확인한다.") void getReservationSlots() { - Long themeId = saveTheme("공포"); - Long dateId = saveDate("2026-06-01"); - Long firstTimeId = saveTime("10:00"); - Long secondTimeId = saveTime("11:00"); - Long reservationSlotId = saveReservationSlot(dateId, firstTimeId, themeId); - saveReservation("보예", reservationSlotId, "CONFIRMED"); + Theme theme = testFixture.saveTheme("공포"); + ReservationDate date = testFixture.saveDate("2026-06-01"); + ReservationTime firstTime = testFixture.saveTime("10:00"); + ReservationTime secondTime = testFixture.saveTime("11:00"); + ReservationSlot reservationSlot = testFixture.saveSlot(date, firstTime, theme); + testFixture.saveReservation("보예", reservationSlot, ReservationStatus.CONFIRMED); given().log().all() .contentType(ContentType.JSON) - .param("themeId", themeId) - .param("dateId", dateId) + .param("themeId", theme.getId()) + .param("dateId", date.getId()) .when().get("/reservation-slots") .then().log().all() .statusCode(200) - .body("[0].timeId", is(firstTimeId.intValue())) + .body("[0].timeId", is(firstTime.getId().intValue())) .body("[0].startAt", is("10:00")) .body("[0].waitingNumber", is(1)) - .body("[1].timeId", is(secondTimeId.intValue())) + .body("[1].timeId", is(secondTime.getId().intValue())) .body("[1].startAt", is("11:00")) .body("[1].waitingNumber", is(0)); } @@ -62,19 +61,19 @@ void getReservationSlots() { @Test @DisplayName("예약 슬롯만 있고 실제 예약이 없으면 예약 인원은 0명으로 조회된다.") void getReservationSlotsWhenReservationSlotHasNoReservation() { - Long themeId = saveTheme("공포"); - Long dateId = saveDate("2026-06-01"); - Long timeId = saveTime("10:00"); - saveReservationSlot(dateId, timeId, themeId); + Theme theme = testFixture.saveTheme("공포"); + ReservationDate date = testFixture.saveDate("2026-06-01"); + ReservationTime time = testFixture.saveTime("10:00"); + testFixture.saveSlot(date, time, theme); given().log().all() .contentType(ContentType.JSON) - .param("themeId", themeId) - .param("dateId", dateId) + .param("themeId", theme.getId()) + .param("dateId", date.getId()) .when().get("/reservation-slots") .then().log().all() .statusCode(200) - .body("[0].timeId", is(timeId.intValue())) + .body("[0].timeId", is(time.getId().intValue())) .body("[0].startAt", is("10:00")) .body("[0].waitingNumber", is(0)); } @@ -95,86 +94,16 @@ void getReservationSlotsWithoutThemeId() { @Test @DisplayName("예약 슬롯 조회 시 존재하지 않는 테마일 경우 404 에러가 발생한다.") void getReservationSlotsWhenThemeNotFound() { - Long dateId = saveDate("2026-06-01"); + ReservationDate date = testFixture.saveDate("2026-06-01"); given().log().all() .contentType(ContentType.JSON) .param("themeId", 999L) - .param("dateId", dateId) + .param("dateId", date.getId()) .when().get("/reservation-slots") .then().log().all() .statusCode(404) .body("code", is("THEME_NOT_EXIST")) .body("message", is("존재하지 않는 테마 입니다.")); } - - private Long saveTheme(String themeName) { - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - themeName, - "무서운 테마", - "theme-url" - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM theme WHERE name = ?", - Long.class, - themeName - ); - } - - private Long saveDate(String date) { - jdbcTemplate.update("INSERT INTO reservation_date(date) VALUES (?)", date); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation_date WHERE date = ?", - Long.class, - date - ); - } - - private Long saveTime(String time) { - jdbcTemplate.update("INSERT INTO reservation_time(start_at) VALUES (?)", time); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation_time WHERE start_at = ?", - Long.class, - time + ":00" - ); - } - - private Long saveReservationSlot(Long dateId, Long timeId, Long themeId) { - jdbcTemplate.update( - "INSERT INTO reservation_slot(date_id, time_id, theme_id) VALUES (?, ?, ?)", - dateId, - timeId, - themeId - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation_slot WHERE date_id = ? AND time_id = ? AND theme_id = ?", - Long.class, - dateId, - timeId, - themeId - ); - } - - private Long saveReservation(String name, Long reservationSlotId, String status) { - jdbcTemplate.update("INSERT INTO users(name) VALUES (?)", name); - Long userId = jdbcTemplate.queryForObject( - "SELECT id FROM users WHERE name = ?", - Long.class, - name - ); - jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " - + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", - userId, - reservationSlotId, - status - ); - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation WHERE user_id = ? AND reservation_slot_id = ?", - Long.class, - userId, - reservationSlotId - ); - } } diff --git a/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java b/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java index af44baeeab..b5d164e9d7 100644 --- a/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java +++ b/src/test/java/roomescape/domain/reservationslot/ReservationSlotServiceTest.java @@ -7,6 +7,7 @@ import static org.mockito.BDDMockito.given; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import java.util.Optional; @@ -17,14 +18,17 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.DataIntegrityViolationException; -import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.reservation.dto.ReservationCountResult; +import roomescape.domain.reservation.JpaReservationRepository; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationStatus; import roomescape.domain.reservationdate.JpaReservationDateRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationslot.dto.ReservationSlotResponse; +import roomescape.domain.reservationtime.JpaReservationTimeRepository; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.JpaThemeRepository; import roomescape.domain.theme.Theme; +import roomescape.domain.user.User; import roomescape.support.exception.NotFoundException; @ExtendWith(MockitoExtension.class) @@ -40,7 +44,10 @@ class ReservationSlotServiceTest { private JpaReservationDateRepository reservationDateRepository; @Mock - private ReservationRepository reservationRepository; + private JpaReservationRepository reservationRepository; + + @Mock + private JpaReservationTimeRepository reservationTimeRepository; @InjectMocks private ReservationSlotService reservationSlotService; @@ -51,12 +58,22 @@ void getReservationSlots() { // given Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); ReservationDate reservationDate = ReservationDate.of(1L, LocalDate.of(2026, 5, 16)); + ReservationTime firstTime = ReservationTime.of(1L, LocalTime.of(10, 0)); + ReservationTime secondTime = ReservationTime.of(2L, LocalTime.of(11, 0)); + ReservationSlot reservationSlot = ReservationSlot.of(1L, reservationDate, firstTime, theme); + LocalDateTime now = LocalDateTime.of(2026, 5, 16, 9, 0); given(themeRepository.findById(theme.getId())).willReturn(Optional.of(theme)); given(reservationDateRepository.findById(reservationDate.getId())).willReturn(Optional.of(reservationDate)); - given(reservationRepository.countReservation(theme.getId(), reservationDate.getId())) + given(reservationTimeRepository.findAllByOrderByStartAtAsc()) + .willReturn(List.of(firstTime, secondTime)); + given(reservationRepository.findReservationsForSlotAvailability( + theme.getId(), + reservationDate.getId(), + ReservationStatus.CANCELED + )) .willReturn(List.of( - ReservationCountResult.of(1L, LocalTime.of(10, 0), 2L), - ReservationCountResult.of(2L, LocalTime.of(11, 0), 0L) + Reservation.of(1L, reservationSlot, User.of(1L, "보예"), ReservationStatus.CONFIRMED, now, now), + Reservation.of(2L, reservationSlot, User.of(2L, "수민"), ReservationStatus.WAITING, now, now) )); // when @@ -115,7 +132,7 @@ void findExistingSlotWhenDuplicateKeyOccurs() { ReservationTime time = ReservationTime.of(1L, LocalTime.of(10, 0)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); ReservationSlot existingSlot = ReservationSlot.of(1L, date, time, theme); - given(reservationSlotRepository.findByScheduleToUpdate(time.getId(), date.getId(), theme.getId())) + given(reservationSlotRepository.findSlotForCreation(time.getId(), date.getId(), theme.getId())) .willReturn(Optional.empty()) .willReturn(Optional.of(existingSlot)); given(reservationSlotRepository.save(any(ReservationSlot.class))) diff --git a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java index 01ccd7d780..29478a4ec1 100644 --- a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java @@ -14,7 +14,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationStatus; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class AdminReservationSlotIntegrationTest { @@ -23,7 +25,7 @@ class AdminReservationSlotIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @Value("${token}") private String adminToken; @@ -31,12 +33,7 @@ class AdminReservationSlotIntegrationTest { @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @@ -77,82 +74,11 @@ void deleteReservation() { .then().log().all() .statusCode(204); - String reservationStatus = jdbcTemplate.queryForObject( - "SELECT status FROM reservation WHERE id = ?", - String.class, - reservationId - ); - assertThat(reservationStatus).isEqualTo("CANCELED"); + assertThat(testFixture.findReservationStatus(reservationId)).isEqualTo(ReservationStatus.CANCELED); } private Long saveThemeDateTimeAndReservation(String name) { - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "공포", - "무서운 테마", - "theme-url" - ); - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - "2026-06-01" - ); - jdbcTemplate.update( - "INSERT INTO reservation_time(start_at) VALUES (?)", - "10:00" - ); - - - Long themeId = jdbcTemplate.queryForObject( - "SELECT id FROM theme WHERE name = ?", - Long.class, - "공포" - ); - Long dateId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_date WHERE date = ?", - Long.class, - "2026-06-01" - ); - Long timeId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_time WHERE start_at = ?", - Long.class, - "10:00:00" - ); - - jdbcTemplate.update( - "INSERT INTO reservation_slot(date_id, time_id, theme_id) VALUES (?, ?, ?)", - dateId, - timeId, - themeId - ); - Long reservationId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_slot WHERE date_id = ? AND time_id = ? AND theme_id = ?", - Long.class, - dateId, - timeId, - themeId - ); - - jdbcTemplate.update("INSERT INTO users(name) VALUES (?)", name); - - Long userId = jdbcTemplate.queryForObject( - "SELECT id FROM users WHERE name = ?", - Long.class, - name - ); - - jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " - + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", - userId, - reservationId, - "CONFIRMED" - ); - - return jdbcTemplate.queryForObject( - "SELECT id FROM reservation WHERE user_id = ? AND reservation_slot_id = ?", - Long.class, - userId, - reservationId - ); + Reservation reservation = testFixture.saveReservation(name, "2026-06-01", "10:00", "공포"); + return reservation.getId(); } } diff --git a/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java index 46d125550d..38418a62df 100644 --- a/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java @@ -15,7 +15,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class AdminReservationTimeIntegrationTest { @@ -24,7 +25,7 @@ class AdminReservationTimeIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @Value("${token}") private String adminToken; @@ -32,21 +33,13 @@ class AdminReservationTimeIntegrationTest { @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @DisplayName("관리자의 예약 시간 전체 조회를 end-to-end로 확인한다.") void getAllReservationTime() { - jdbcTemplate.update( - "INSERT INTO reservation_time(start_at) VALUES (?)", - "10:10" - ); + testFixture.saveTime("10:10"); given().log().all() .contentType(ContentType.JSON) @@ -133,21 +126,12 @@ void createReservationTimeWithoutToken() { @Test @DisplayName("관리자의 예약 시간 삭제를 end-to-end로 확인한다.") void deleteReservationTime() { - jdbcTemplate.update( - "INSERT INTO reservation_time(start_at) VALUES (?)", - "10:10" - ); - - Long timeId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_time WHERE start_at = ?", - Long.class, - "10:10:00" - ); + ReservationTime reservationTime = testFixture.saveTime("10:10"); given().log().all() .contentType(ContentType.JSON) .header("X-ADMIN-TOKEN", adminToken) - .when().delete("/admin/times/{id}", timeId) + .when().delete("/admin/times/{id}", reservationTime.getId()) .then().log().all() .statusCode(200); diff --git a/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java b/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java index 0a558db61a..a9d26e24ae 100644 --- a/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java @@ -13,7 +13,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.reservation.ReservationStatus; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationslot.ReservationSlot; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ThemeIntegrationTest { @@ -22,28 +26,18 @@ class ThemeIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); - jdbcTemplate.update("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1"); - jdbcTemplate.update("ALTER TABLE reservation_slot ALTER COLUMN id RESTART WITH 1"); + testFixture.clear(); } @Test @DisplayName("전체 테마 조회를 end-to-end로 확인한다.") void getAllTheme() { - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "공포", "무서운 테마", "theme-url" - ); + testFixture.saveTheme("공포"); given().log().all() .contentType(ContentType.JSON) .when().get("/themes") @@ -58,59 +52,13 @@ void getAllTheme() { @DisplayName("인기 테마 조회는 예약 슬롯 id와 예약 id가 달라도 실제 예약 슬롯 기준으로 집계한다.") void getThemeRankByReservationSlotId() { LocalDate reservationDate = LocalDate.now().minusDays(1); - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "공포", "무서운 테마", "theme-url" - ); - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "보예", "보예 테마", "boye-url" - ); - jdbcTemplate.update( - "INSERT INTO reservation_date(date) VALUES (?)", - reservationDate - ); - jdbcTemplate.update( - "INSERT INTO reservation_time(start_at) VALUES (?)", - "10:00" - ); - Long themeId = jdbcTemplate.queryForObject("SELECT id FROM theme WHERE name = ?", Long.class, "공포"); - Long dummyThemeId = jdbcTemplate.queryForObject("SELECT id FROM theme WHERE name = ?", Long.class, "보예"); - Long dateId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_date WHERE date = ?", - Long.class, - reservationDate - ); - Long timeId = jdbcTemplate.queryForObject( - "SELECT id FROM reservation_time WHERE start_at = ?", - Long.class, - "10:00:00" - ); - jdbcTemplate.update( - "INSERT INTO reservation_slot(date_id, time_id, theme_id) VALUES (?, ?, ?)", - dateId, - timeId, - dummyThemeId - ); - jdbcTemplate.update( - "INSERT INTO reservation_slot(date_id, time_id, theme_id) VALUES (?, ?, ?)", - dateId, - timeId, - themeId - ); - Long targetSlotId = jdbcTemplate.queryForObject( - "SELECT MAX(id) FROM reservation_slot", - Long.class - ); - jdbcTemplate.update("INSERT INTO users(name) VALUES (?)", "보예"); - Long userId = jdbcTemplate.queryForObject("SELECT id FROM users WHERE name = ?", Long.class, "보예"); - jdbcTemplate.update( - "INSERT INTO reservation(user_id, reservation_slot_id, status, created_at, updated_at) " - + "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", - userId, - targetSlotId, - "CONFIRMED" - ); + Theme theme = testFixture.saveTheme("공포"); + Theme dummyTheme = testFixture.saveTheme("보예", "보예 테마", "boye-url"); + ReservationDate date = testFixture.saveDate(reservationDate.toString()); + ReservationTime time = testFixture.saveTime("10:00"); + testFixture.saveSlot(date, time, dummyTheme); + ReservationSlot targetSlot = testFixture.saveSlot(date, time, theme); + testFixture.saveReservation("보예", targetSlot, ReservationStatus.CONFIRMED); given().log().all() .contentType(ContentType.JSON) diff --git a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java index 3c180e95fb..87c513267f 100644 --- a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java @@ -1,6 +1,7 @@ package roomescape.domain.theme; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.tuple; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -8,18 +9,22 @@ import java.time.Clock; import java.time.Instant; +import java.time.LocalDate; import java.time.ZoneId; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import roomescape.domain.reservation.JpaReservationRepository; +import roomescape.domain.reservation.ReservationStatus; import roomescape.domain.reservationslot.JpaReservationSlotRepository; import roomescape.domain.theme.admin.dto.AdminThemeResponse; import roomescape.domain.theme.admin.dto.CreateThemeRequest; import roomescape.domain.theme.admin.dto.CreateThemeResponse; +import roomescape.domain.theme.dto.ThemeRankResponse; import roomescape.domain.theme.dto.ThemeResponse; @ExtendWith(MockitoExtension.class) @@ -32,16 +37,25 @@ class ThemeServiceTest { private JpaReservationSlotRepository reservationSlotRepository; @Mock - private ThemeRepository themeQueryRepository; + private JpaReservationRepository reservationRepository; private final Clock clock = Clock.fixed( Instant.parse("2026-05-27T00:00:00Z"), ZoneId.of("Asia/Seoul") ); - @InjectMocks private ThemeService themeService; + @BeforeEach + void setUp() { + themeService = new ThemeService( + themeRepository, + reservationSlotRepository, + reservationRepository, + clock + ); + } + @Test @DisplayName("관리자용 테마 목록을 조회한다.") void getThemeListForAdmin() { @@ -118,4 +132,40 @@ void deleteTheme() { // then verify(themeRepository).deleteById(themeId); } + + @Test + @DisplayName("예약된 테마 목록으로 인기 테마 순위를 계산한다.") + void getThemeRank() { + // given + Theme firstTheme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); + Theme secondTheme = Theme.of(2L, "추리", "추리 테마", "mystery-url"); + Theme thirdTheme = Theme.of(3L, "잠입", "잠입 테마", "escape-url"); + given(reservationRepository.findThemesForRanking( + LocalDate.of(2026, 5, 20), + LocalDate.of(2026, 5, 27), + ReservationStatus.CANCELED + )).willReturn(List.of( + secondTheme, + firstTheme, + firstTheme, + thirdTheme, + thirdTheme + )); + + // when + List responses = themeService.getThemeRank(); + + // then + assertThat(responses) + .extracting( + ThemeRankResponse::id, + ThemeRankResponse::themeName, + ThemeRankResponse::rank + ) + .containsExactly( + tuple(1L, "공포", 1), + tuple(3L, "잠입", 1), + tuple(2L, "추리", 3) + ); + } } diff --git a/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java b/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java index 7b09bba0d8..0a073d44e2 100644 --- a/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java +++ b/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java @@ -15,7 +15,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.jdbc.core.JdbcTemplate; +import roomescape.domain.theme.Theme; +import roomescape.support.TestFixture; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class AdminThemeIntegrationTest { @@ -24,7 +25,7 @@ class AdminThemeIntegrationTest { private int port; @Autowired - private JdbcTemplate jdbcTemplate; + private TestFixture testFixture; @Value("${token}") private String adminToken; @@ -32,21 +33,13 @@ class AdminThemeIntegrationTest { @BeforeEach void setUp() { RestAssured.port = port; - jdbcTemplate.update("DELETE FROM reservation"); - jdbcTemplate.update("DELETE FROM reservation_slot"); - jdbcTemplate.update("DELETE FROM users"); - jdbcTemplate.update("DELETE FROM reservation_date"); - jdbcTemplate.update("DELETE FROM reservation_time"); - jdbcTemplate.update("DELETE FROM theme"); + testFixture.clear(); } @Test @DisplayName("관리자의 테마 전체 조회를 end-to-end로 확인한다.") void getAllThemeForAdmin() { - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "공포", "무서운 테마", "theme-url" - ); + testFixture.saveTheme("공포"); given().log().all() .contentType(ContentType.JSON) @@ -145,21 +138,12 @@ void createThemeWithOutToken() { @Test @DisplayName("관리자의 테마 삭제를 end-to-end로 확인한다.") void deleteTheme() { - jdbcTemplate.update( - "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", - "공포", "무서운 테마", "theme-url" - ); - - Long themeId = jdbcTemplate.queryForObject( - "SELECT id FROM theme WHERE name = ?", - Long.class, - "공포" - ); + Theme theme = testFixture.saveTheme("공포"); given().log().all() .contentType(ContentType.JSON) .header("X-ADMIN-TOKEN", adminToken) - .when().delete("/admin/themes/{id}", themeId) + .when().delete("/admin/themes/{id}", theme.getId()) .then().log().all() .statusCode(204); diff --git a/src/test/java/roomescape/support/TestFixture.java b/src/test/java/roomescape/support/TestFixture.java new file mode 100644 index 0000000000..7c02f3c6c5 --- /dev/null +++ b/src/test/java/roomescape/support/TestFixture.java @@ -0,0 +1,131 @@ +package roomescape.support; + +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import org.springframework.stereotype.Component; +import roomescape.domain.reservation.JpaReservationRepository; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationStatus; +import roomescape.domain.reservationdate.JpaReservationDateRepository; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationslot.JpaReservationSlotRepository; +import roomescape.domain.reservationslot.ReservationSlot; +import roomescape.domain.reservationtime.JpaReservationTimeRepository; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.JpaThemeRepository; +import roomescape.domain.theme.Theme; +import roomescape.domain.user.JpaUserRepository; +import roomescape.domain.user.User; + +@Component +public class TestFixture { + + private static final LocalDateTime DEFAULT_TIME = LocalDateTime.of(2026, 1, 1, 0, 0); + + private final JpaReservationRepository reservationRepository; + private final JpaReservationSlotRepository reservationSlotRepository; + private final JpaUserRepository userRepository; + private final JpaReservationDateRepository reservationDateRepository; + private final JpaReservationTimeRepository reservationTimeRepository; + private final JpaThemeRepository themeRepository; + + public TestFixture( + JpaReservationRepository reservationRepository, + JpaReservationSlotRepository reservationSlotRepository, + JpaUserRepository userRepository, + JpaReservationDateRepository reservationDateRepository, + JpaReservationTimeRepository reservationTimeRepository, + JpaThemeRepository themeRepository + ) { + this.reservationRepository = reservationRepository; + this.reservationSlotRepository = reservationSlotRepository; + this.userRepository = userRepository; + this.reservationDateRepository = reservationDateRepository; + this.reservationTimeRepository = reservationTimeRepository; + this.themeRepository = themeRepository; + } + + public void clear() { + reservationRepository.deleteAllInBatch(); + reservationSlotRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + reservationDateRepository.deleteAllInBatch(); + reservationTimeRepository.deleteAllInBatch(); + themeRepository.deleteAllInBatch(); + } + + public Theme saveTheme(String name) { + return saveTheme(name, "무서운 테마", "theme-url"); + } + + public Theme saveTheme(String name, String content, String url) { + return themeRepository.save(Theme.createWithoutId(name, content, url)); + } + + public ReservationDate saveDate(String date) { + return reservationDateRepository.save(ReservationDate.createWithoutId(LocalDate.parse(date))); + } + + public ReservationTime saveTime(String time) { + return reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.parse(time))); + } + + public ReservationSlot saveSlot(Long dateId, Long timeId, Long themeId) { + ReservationDate date = reservationDateRepository.findById(dateId).orElseThrow(); + ReservationTime time = reservationTimeRepository.findById(timeId).orElseThrow(); + Theme theme = themeRepository.findById(themeId).orElseThrow(); + return saveSlot(date, time, theme); + } + + public ReservationSlot saveSlot(ReservationDate date, ReservationTime time, Theme theme) { + return reservationSlotRepository.save(ReservationSlot.createWithoutId(date, time, theme)); + } + + public User saveUser(String name) { + return userRepository.findByName(name) + .orElseGet(() -> userRepository.save(User.createWithoutId(name))); + } + + public Reservation saveReservation(String name, ReservationSlot reservationSlot, ReservationStatus status) { + return saveReservation(name, reservationSlot, status, DEFAULT_TIME); + } + + public Reservation saveReservation( + String name, + ReservationSlot reservationSlot, + ReservationStatus status, + LocalDateTime dateTime + ) { + User user = saveUser(name); + return reservationRepository.save( + Reservation.createWithoutId(reservationSlot, user, status, fixedClockAt(dateTime)) + ); + } + + public Reservation saveReservation(String name, Long reservationSlotId, ReservationStatus status) { + ReservationSlot reservationSlot = reservationSlotRepository.findById(reservationSlotId).orElseThrow(); + return saveReservation(name, reservationSlot, status); + } + + public Reservation saveReservation(String name, String date, String time, String themeName) { + Theme theme = saveTheme(themeName); + ReservationDate reservationDate = saveDate(date); + ReservationTime reservationTime = saveTime(time); + ReservationSlot reservationSlot = saveSlot(reservationDate, reservationTime, theme); + return saveReservation(name, reservationSlot, ReservationStatus.CONFIRMED); + } + + public ReservationStatus findReservationStatus(Long reservationId) { + return reservationRepository.findById(reservationId) + .orElseThrow() + .getStatus(); + } + + private Clock fixedClockAt(LocalDateTime dateTime) { + ZoneId zoneId = ZoneId.systemDefault(); + return Clock.fixed(dateTime.atZone(zoneId).toInstant(), zoneId); + } +} From 91ad64d919e2b9c20de8e4aa266ca0032e5ce6aa Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 18 Jun 2026 16:10:56 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/JpaReservationRepository.java | 13 +++++ .../reservation/ReservationController.java | 6 +-- .../reservation/ReservationService.java | 14 ++++++ .../admin/AdminReservationController.java | 8 ++-- .../ReservationControllerTest.java | 12 ++--- .../ReservationIntegrationTest.java | 20 ++++---- .../admin/AdminReservationControllerTest.java | 47 ++++++++---------- .../AdminReservationSlotIntegrationTest.java | 48 ++++++++----------- 8 files changed, 90 insertions(+), 78 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java index 0fc9999a98..ff878d8f1c 100644 --- a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java @@ -49,6 +49,19 @@ List findWaitingReservationsForPromotion( """) List findReservationsForAdmin(); + @Query(""" + select reservation + from Reservation reservation + join fetch reservation.user + join fetch reservation.reservationSlot slot + join fetch slot.date date + join fetch slot.time time + join fetch slot.theme + where reservation.status = :status + order by date.date desc, time.startAt desc, reservation.id + """) + List findWaitingReservationsForAdmin(ReservationStatus status); + @Query(""" select reservation from Reservation reservation diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index 230caee8df..470c73d815 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -26,7 +26,7 @@ public class ReservationController { private final ReservationService reservationService; - @PostMapping("/reservations") + @PostMapping("/waitings") public ResponseEntity createReservation( @Valid @RequestBody CreateReservationRequest request ) { @@ -34,7 +34,7 @@ public ResponseEntity createReservation( return ResponseEntity.status(HttpStatus.CREATED).body(response); } - @GetMapping("/reservations") + @GetMapping("/reservations-mine") public ResponseEntity getUserReservations( @RequestParam @NotBlank(message = "예약자 이름은 필수 입력값 입니다.") @@ -44,7 +44,7 @@ public ResponseEntity getUserReservations( return ResponseEntity.ok(response); } - @DeleteMapping("/reservations/{id}") + @DeleteMapping("/waitings/{id}") public ResponseEntity cancelUserReservation( @PathVariable Long id ) { diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index bd9b0f69e4..c239d54e8a 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -66,6 +66,20 @@ public List getAllReservations() { .toList(); } + public List getWaitingReservations() { + List reservations = reservationRepository.findWaitingReservationsForAdmin( + ReservationStatus.WAITING + ); + Map waitingNumberByReservationId = calculateWaitingNumbers(reservations); + return reservations.stream() + .map(reservation -> new ReservationWithWaitingNumber( + reservation, + waitingNumberByReservationId.get(reservation.getId()) + )) + .map(ReservationResponse::from) + .toList(); + } + public UserReservationsResponse getUserReservations(String username) { List reservations = reservationRepository.findUserReservations(username); Map waitingNumberByReservationId = calculateWaitingNumbersInSameSlots(reservations); diff --git a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java index 756573515f..3a2eacc59b 100644 --- a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java +++ b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java @@ -20,16 +20,16 @@ public class AdminReservationController { private final ReservationService reservationService; private final AdminRequestValidator validator; - @GetMapping("/admin/reservations") - public ResponseEntity> getAllReservation(HttpServletRequest request) { + @GetMapping("/admin/waitings") + public ResponseEntity> getWaitingReservations(HttpServletRequest request) { if (validator.isUnauthorized(request)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - List response = reservationService.getAllReservations(); + List response = reservationService.getWaitingReservations(); return ResponseEntity.ok(response); } - @DeleteMapping("/admin/reservations/{id}") + @DeleteMapping("/admin/waitings/{id}") public ResponseEntity cancelReservation(HttpServletRequest request, @PathVariable Long id) { if (validator.isUnauthorized(request)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); diff --git a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java index 34253db234..67c377fb32 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java @@ -68,7 +68,7 @@ void createReservation() throws Exception { .willReturn(response); // when & then - mockMvc.perform(post("/reservations") + mockMvc.perform(post("/waitings") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) @@ -92,7 +92,7 @@ void createWrongParameterReservation() throws Exception { ); // when & then - mockMvc.perform(post("/reservations") + mockMvc.perform(post("/waitings") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -124,7 +124,7 @@ void getUserReservations() throws Exception { given(reservationService.getUserReservations(name)).willReturn(response); // when & then - mockMvc.perform(get("/reservations") + mockMvc.perform(get("/reservations-mine") .contentType(MediaType.APPLICATION_JSON) .param("name", name)) .andExpect(status().isOk()) @@ -143,7 +143,7 @@ void getUserReservations() throws Exception { @DisplayName("예약자 이름 조회 시 이름이 누락되면 예외가 발생한다.") void getUserReservationsWithoutName() throws Exception { // given & when & then - mockMvc.perform(get("/reservations") + mockMvc.perform(get("/reservations-mine") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value("REQUIRED_PARAMETER_MISSING")) @@ -157,7 +157,7 @@ void cancelUserReservation() throws Exception { Long id = 1L; // when & then - mockMvc.perform(delete("/reservations/{id}", id)) + mockMvc.perform(delete("/waitings/{id}", id)) .andExpect(status().isNoContent()); verify(reservationService).cancelUserReservation(id); @@ -228,7 +228,7 @@ void updateReservationWhenReservationNotFound() throws Exception { @DisplayName("예약 삭제 시 id를 누락한 경우 예외가 발생한다.") void cancelUserReservationWithoutId() throws Exception { // given & when & then - mockMvc.perform(delete("/reservations")) + mockMvc.perform(delete("/waitings")) .andExpect(status().isMethodNotAllowed()); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java index 18f18ee46a..204fdee6ef 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java @@ -83,7 +83,7 @@ void createReservation() { given().log().all() .contentType(ContentType.JSON) .body(request) - .when().post("/reservations") + .when().post("/waitings") .then().log().all() .statusCode(201) .body("date", is("2026-06-01")) @@ -95,7 +95,7 @@ void createReservation() { given() .contentType(ContentType.JSON) .param("name", "보예") - .when().get("/reservations") + .when().get("/reservations-mine") .then() .statusCode(200) .body("username", is("보예")) @@ -119,7 +119,7 @@ void createReservationWithoutTimeId() { given().log().all() .contentType(ContentType.JSON) .body(request) - .when().post("/reservations") + .when().post("/waitings") .then().log().all() .statusCode(400) .body("code", is("INPUT_VALIDATION_ERROR")) @@ -134,7 +134,7 @@ void getUserReservations() { given().log().all() .contentType(ContentType.JSON) .param("name", "보예") - .when().get("/reservations") + .when().get("/reservations-mine") .then().log().all() .statusCode(200) .body("username", is("보예")) @@ -158,7 +158,7 @@ void getUserReservationsWithWaitingNumberInSameSlot() { given().log().all() .contentType(ContentType.JSON) .param("name", "말랑") - .when().get("/reservations") + .when().get("/reservations-mine") .then().log().all() .statusCode(200) .body("username", is("말랑")) @@ -171,7 +171,7 @@ void getUserReservationsWithWaitingNumberInSameSlot() { void getUserReservationsWithoutName() { given().log().all() .contentType(ContentType.JSON) - .when().get("/reservations") + .when().get("/reservations-mine") .then().log().all() .statusCode(400) .body("code", is("REQUIRED_PARAMETER_MISSING")) @@ -185,14 +185,14 @@ void deleteUserReservation() { given().log().all() .contentType(ContentType.JSON) - .when().delete("/reservations/{id}", reservationId) + .when().delete("/waitings/{id}", reservationId) .then().log().all() .statusCode(204); given() .contentType(ContentType.JSON) .param("name", "보예") - .when().get("/reservations") + .when().get("/reservations-mine") .then() .statusCode(200) .body("username", is("보예")) @@ -231,7 +231,7 @@ void checkRollback() { // when & then given(). contentType(ContentType.JSON) - .when().delete("/reservations/{id}", confirmedReservationId) + .when().delete("/waitings/{id}", confirmedReservationId) .then() .statusCode(500); @@ -266,7 +266,7 @@ void updateReservation() { given() .contentType(ContentType.JSON) .param("name", "보예") - .when().get("/reservations") + .when().get("/reservations-mine") .then() .statusCode(200) .body("reservations[0].reservationSlot.date.startWhen", is("2026-06-02")) diff --git a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationControllerTest.java b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationControllerTest.java index 77e9638b11..3527d89ceb 100644 --- a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationControllerTest.java +++ b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationControllerTest.java @@ -45,8 +45,8 @@ class AdminReservationControllerTest { private AdminRequestValidator validator; @Test - @DisplayName("관리자가 전체 예약 조회 시 요청과 응답을 확인한다.") - void getAllReservation() throws Exception { + @DisplayName("관리자가 대기 목록 조회 시 요청과 응답을 확인한다.") + void getWaitingReservations() throws Exception { // given ReservationResponse response = new ReservationResponse( 1L, @@ -54,58 +54,49 @@ void getAllReservation() throws Exception { ReservationTimePayload.from(ReservationTime.of(2L, LocalTime.of(10, 10))), ThemePayload.from(Theme.of(3L, "공포", "으악 무서워!", "theme-url")), "보예", - null, - ReservationStatus.CONFIRMED + 1L, + ReservationStatus.WAITING ); when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); - given(reservationService.getAllReservations()) + given(reservationService.getWaitingReservations()) .willReturn(List.of(response)); // when & then - mockMvc.perform(get("/admin/reservations") + mockMvc.perform(get("/admin/waitings") .contentType(MediaType.APPLICATION_JSON) .header("X-ADMIN-TOKEN", "token")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].id").value(1)) - .andExpect(jsonPath("$[0].date").value("2026-05-10")) - .andExpect(jsonPath("$[0].time.id").value(2)) - .andExpect(jsonPath("$[0].time.startAt").value("10:10")) - .andExpect(jsonPath("$[0].theme.id").value(3)) - .andExpect(jsonPath("$[0].theme.name").value("공포")) - .andExpect(jsonPath("$[0].theme.content").value("으악 무서워!")) - .andExpect(jsonPath("$[0].theme.url").value("theme-url")) - .andExpect(jsonPath("$[0].userName").value("보예")) - .andExpect(jsonPath("$[0].waitingNumber").doesNotExist()) - .andExpect(jsonPath("$[0].reservationStatus").value("CONFIRMED")) - ; - - verify(reservationService).getAllReservations(); + .andExpect(jsonPath("$[0].waitingNumber").value(1)) + .andExpect(jsonPath("$[0].reservationStatus").value("WAITING")); + + verify(reservationService).getWaitingReservations(); } @Test - @DisplayName("관리자 인증에 실패하면 전체 예약 조회 시 401을 반환한다.") - void getAllReservationWhenUnauthorized() throws Exception { + @DisplayName("관리자 인증에 실패하면 대기 목록 조회 시 401을 반환한다.") + void getWaitingReservationsWhenUnauthorized() throws Exception { // given when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); // when & then - mockMvc.perform(get("/admin/reservations") + mockMvc.perform(get("/admin/waitings") .contentType(MediaType.APPLICATION_JSON) .header("X-ADMIN-TOKEN", "wrong-token")) .andExpect(status().isUnauthorized()); - verify(reservationService, never()).getAllReservations(); + verify(reservationService, never()).getWaitingReservations(); } @Test - @DisplayName("관리자가 예약 삭제 시 요청과 응답을 확인한다.") - void cancelReservation() throws Exception { + @DisplayName("관리자가 대기 예약 삭제 시 요청과 응답을 확인한다.") + void cancelWaitingReservation() throws Exception { // given Long id = 1L; when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); // when & then - mockMvc.perform(delete("/admin/reservations/{id}", id) + mockMvc.perform(delete("/admin/waitings/{id}", id) .header("X-ADMIN-TOKEN", "token")) .andExpect(status().isNoContent()); @@ -113,14 +104,14 @@ void cancelReservation() throws Exception { } @Test - @DisplayName("관리자 인증에 실패하면 예약 삭제 시 401을 반환한다.") + @DisplayName("관리자 인증에 실패하면 대기 예약 삭제 시 401을 반환한다.") void cancelReservationWhenUnauthorized() throws Exception { // given Long id = 1L; when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); // when & then - mockMvc.perform(delete("/admin/reservations/{id}", id) + mockMvc.perform(delete("/admin/waitings/{id}", id) .header("X-ADMIN-TOKEN", "wrong-token")) .andExpect(status().isUnauthorized()); diff --git a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java index 29478a4ec1..eb812a0c36 100644 --- a/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservationslot/admin/AdminReservationSlotIntegrationTest.java @@ -14,7 +14,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationStatus; import roomescape.support.TestFixture; @@ -37,48 +36,43 @@ void setUp() { } @Test - @DisplayName("관리자의 예약 전체 조회를 end-to-end로 확인한다.") - void getAllReservation() { - saveThemeDateTimeAndReservation("보예"); + @DisplayName("관리자의 대기 목록 조회를 end-to-end로 확인한다.") + void getWaitingReservations() { + var theme = testFixture.saveTheme("공포"); + var date = testFixture.saveDate("2026-06-01"); + var time = testFixture.saveTime("10:00"); + var slot = testFixture.saveSlot(date, time, theme); + testFixture.saveReservation("보예", slot, ReservationStatus.CONFIRMED); + testFixture.saveReservation("수민", slot, ReservationStatus.WAITING); given().log().all() .contentType(ContentType.JSON) .header("X-ADMIN-TOKEN", adminToken) - .when().get("/admin/reservations") + .when().get("/admin/waitings") .then().log().all() .statusCode(200) - .body("[0].date", is("2026-06-01")) - .body("[0].time.startAt", is("10:00")) - .body("[0].theme.name", is("공포")); + .body("[0].theme.name", is("공포")) + .body("[0].userName", is("수민")) + .body("[0].waitingNumber", is(1)) + .body("[0].reservationStatus", is("WAITING")); } @Test - @DisplayName("관리자가 토큰을 누락했을 경우 401 예외가 발생한다.") - void getAllReservationWithoutToken() { - given().log().all() - .contentType(ContentType.JSON) - .when().get("/admin/reservations") - .then().log().all() - .statusCode(401); - } - - @Test - @DisplayName("관리자의 예약 취소를 end-to-end로 확인한다.") - void deleteReservation() { - Long reservationId = saveThemeDateTimeAndReservation("보예"); + @DisplayName("관리자의 대기 예약 취소를 end-to-end로 확인한다.") + void deleteWaitingReservation() { + var theme = testFixture.saveTheme("공포"); + var date = testFixture.saveDate("2026-06-01"); + var time = testFixture.saveTime("10:00"); + var slot = testFixture.saveSlot(date, time, theme); + Long reservationId = testFixture.saveReservation("보예", slot, ReservationStatus.WAITING).getId(); given().log().all() .contentType(ContentType.JSON) .header("X-ADMIN-TOKEN", adminToken) - .when().delete("/admin/reservations/{id}", reservationId) + .when().delete("/admin/waitings/{id}", reservationId) .then().log().all() .statusCode(204); assertThat(testFixture.findReservationStatus(reservationId)).isEqualTo(ReservationStatus.CANCELED); } - - private Long saveThemeDateTimeAndReservation(String name) { - Reservation reservation = testFixture.saveReservation(name, "2026-06-01", "10:00", "공포"); - return reservation.getId(); - } } From b1ca731cd3700ca32b86702e6f2e2e6f1d6ebf4d Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 18 Jun 2026 16:19:22 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=ED=9A=8C=20=EC=88=9C=EB=B2=88=EC=9D=84=20?= =?UTF-8?q?JPQL=20projection=EC=9C=BC=EB=A1=9C=20=EA=B3=84=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/JpaReservationRepository.java | 107 ++++++++++++------ .../reservation/ReservationService.java | 72 +----------- 2 files changed, 78 insertions(+), 101 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java index ff878d8f1c..8e23d10ee9 100644 --- a/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JpaReservationRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import roomescape.domain.reservation.dto.ReservationWithWaitingNumber; import roomescape.domain.theme.Theme; public interface JpaReservationRepository extends JpaRepository { @@ -38,55 +39,93 @@ List findWaitingReservationsForPromotion( ); @Query(""" - select reservation + select new roomescape.domain.reservation.dto.ReservationWithWaitingNumber( + reservation, + case + when reservation.status = :waitingStatus then ( + select count(waitingReservation) + 1 + from Reservation waitingReservation + where waitingReservation.reservationSlot = slot + and waitingReservation.status = :waitingStatus + and ( + waitingReservation.updatedAt < reservation.updatedAt + or ( + waitingReservation.updatedAt = reservation.updatedAt + and waitingReservation.id < reservation.id + ) + ) + ) + else null + end + ) from Reservation reservation - join fetch reservation.user - join fetch reservation.reservationSlot slot - join fetch slot.date date - join fetch slot.time time - join fetch slot.theme + join reservation.user + join reservation.reservationSlot slot + join slot.date date + join slot.time time + join slot.theme order by date.date desc, time.startAt desc, reservation.id """) - List findReservationsForAdmin(); + List findReservationsForAdmin(ReservationStatus waitingStatus); @Query(""" - select reservation + select new roomescape.domain.reservation.dto.ReservationWithWaitingNumber( + reservation, + ( + select count(waitingReservation) + 1 + from Reservation waitingReservation + where waitingReservation.reservationSlot = slot + and waitingReservation.status = :status + and ( + waitingReservation.updatedAt < reservation.updatedAt + or ( + waitingReservation.updatedAt = reservation.updatedAt + and waitingReservation.id < reservation.id + ) + ) + ) + ) from Reservation reservation - join fetch reservation.user - join fetch reservation.reservationSlot slot - join fetch slot.date date - join fetch slot.time time - join fetch slot.theme + join reservation.user + join reservation.reservationSlot slot + join slot.date date + join slot.time time + join slot.theme where reservation.status = :status order by date.date desc, time.startAt desc, reservation.id """) - List findWaitingReservationsForAdmin(ReservationStatus status); + List findWaitingReservationsForAdmin(ReservationStatus status); @Query(""" - select reservation + select new roomescape.domain.reservation.dto.ReservationWithWaitingNumber( + reservation, + case + when reservation.status = :waitingStatus then ( + select count(waitingReservation) + 1 + from Reservation waitingReservation + where waitingReservation.reservationSlot = slot + and waitingReservation.status = :waitingStatus + and ( + waitingReservation.updatedAt < reservation.updatedAt + or ( + waitingReservation.updatedAt = reservation.updatedAt + and waitingReservation.id < reservation.id + ) + ) + ) + else null + end + ) from Reservation reservation - join fetch reservation.user user - join fetch reservation.reservationSlot slot - join fetch slot.date date - join fetch slot.time time - join fetch slot.theme + join reservation.user user + join reservation.reservationSlot slot + join slot.date date + join slot.time time + join slot.theme where user.name = :username order by date.date desc, time.startAt desc, reservation.id """) - List findUserReservations(String username); - - @Query(""" - select reservation - from Reservation reservation - join fetch reservation.reservationSlot slot - where slot.id in :reservationSlotIds - and reservation.status = :status - order by slot.id, reservation.updatedAt, reservation.id - """) - List findWaitingReservationsInSlots( - List reservationSlotIds, - ReservationStatus status - ); + List findUserReservations(String username, ReservationStatus waitingStatus); @Query(""" select reservation diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index c239d54e8a..3bc30ad197 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -4,10 +4,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,82 +58,23 @@ public CreateReservationResponse createReservation(CreateReservationRequest requ } public List getAllReservations() { - return findAllWithWaitingNumber().stream() + return reservationRepository.findReservationsForAdmin(ReservationStatus.WAITING).stream() .map(ReservationResponse::from) .toList(); } public List getWaitingReservations() { - List reservations = reservationRepository.findWaitingReservationsForAdmin( - ReservationStatus.WAITING - ); - Map waitingNumberByReservationId = calculateWaitingNumbers(reservations); - return reservations.stream() - .map(reservation -> new ReservationWithWaitingNumber( - reservation, - waitingNumberByReservationId.get(reservation.getId()) - )) + return reservationRepository.findWaitingReservationsForAdmin(ReservationStatus.WAITING).stream() .map(ReservationResponse::from) .toList(); } public UserReservationsResponse getUserReservations(String username) { - List reservations = reservationRepository.findUserReservations(username); - Map waitingNumberByReservationId = calculateWaitingNumbersInSameSlots(reservations); - List userReservations = reservations.stream() - .map(reservation -> new ReservationWithWaitingNumber( - reservation, - waitingNumberByReservationId.get(reservation.getId()) - )) - .toList(); - return UserReservationsResponse.of(username, userReservations); - } - - private List findAllWithWaitingNumber() { - List reservations = reservationRepository.findReservationsForAdmin(); - Map waitingNumberByReservationId = calculateWaitingNumbers(reservations); - return reservations.stream() - .map(reservation -> new ReservationWithWaitingNumber( - reservation, - waitingNumberByReservationId.get(reservation.getId()) - )) - .toList(); - } - - private Map calculateWaitingNumbers(List reservations) { - Map waitingNumberByReservationId = new HashMap<>(); - Map nextWaitingNumberBySlotId = new HashMap<>(); - - reservations.stream() - .filter(reservation -> reservation.getStatus() == ReservationStatus.WAITING) - .sorted(Comparator - .comparing((Reservation reservation) -> reservation.getReservationSlot().getId()) - .thenComparing(Reservation::getUpdatedAt) - .thenComparing(Reservation::getId)) - .forEach(reservation -> { - Long slotId = reservation.getReservationSlot().getId(); - Long waitingNumber = nextWaitingNumberBySlotId.merge(slotId, 1L, Long::sum); - waitingNumberByReservationId.put(reservation.getId(), waitingNumber); - }); - - return waitingNumberByReservationId; - } - - private Map calculateWaitingNumbersInSameSlots(List reservations) { - List reservationSlotIds = reservations.stream() - .map(reservation -> reservation.getReservationSlot().getId()) - .distinct() - .toList(); - - if (reservationSlotIds.isEmpty()) { - return Map.of(); - } - - List waitingReservations = reservationRepository.findWaitingReservationsInSlots( - reservationSlotIds, + List userReservations = reservationRepository.findUserReservations( + username, ReservationStatus.WAITING ); - return calculateWaitingNumbers(waitingReservations); + return UserReservationsResponse.of(username, userReservations); } @Transactional